Compare commits

..

72 Commits

Author SHA1 Message Date
Patrick Goldinger
f41385ae75 Adjust setup script 2024-10-20 18:46:48 +02:00
Patrick Goldinger
5c85be61d9 Add vscode env setup script 2024-10-20 18:46:41 +02:00
Patrick Goldinger
639beb9e64 Add initial flest implementation 2024-10-20 18:46:33 +02:00
Patrick Goldinger
453fb0253a Fix predictive back by removing generic onBackPressedHandler (#2646) 2024-10-19 15:13:35 +02:00
Lucas Sanginetto
13fc7679a2 Fix incorrect bracket labels in IPA symbols layout (#2644) 2024-10-19 11:26:25 +02:00
Patrick Goldinger
2421d13038 Update ROADMAP.md (#2639) 2024-10-18 00:34:15 +02:00
florisboard-bot
7dedfd4f7a Update translations from Crowdin 2024-10-17 19:07:32 +02:00
Patrick Goldinger
ef37194900 Disable compose strong skipping mode (#2637) 2024-10-17 16:51:02 +02:00
Lars Mühlbauer
58134b1ceb Add fix for sensitive clipboard suggestions (#2635) 2024-10-16 23:17:54 +02:00
Lars Mühlbauer
53cfbad404 Clipboard History enhancements (#2631)
* Hide sensitive clip data in clipboard history

* Add is remoteDevice flag

* Do not link password length to displayed characters

* Add backspace in clipboard history (#2615)

* Use ClipboardItem level function for the obfuscation of the text

* Move the backspace button to the header bar

* Adjust innerHeight to match the full layout

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

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

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

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

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

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

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

* Remove translations due to contribution guidelines

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

* Revert spacing in values-ru/strings.xml

---------

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

* Fix history import error when files are already in ClipboardFileStorage

* Ahhhh yes, the nitpicks `:DD`

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

---------

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

* Add review suggestions

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

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

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

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

* Apply suggestions from code review

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

---------

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

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

View File

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

View File

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

View File

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

View File

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

5
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -14,11 +14,13 @@
* limitations under the License.
*/
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.agp.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mannodermaus.android.junit5)
@@ -32,7 +34,7 @@ val projectBuildToolsVersion: String by project
val projectNdkVersion: String by project
val projectVersionCode: String by project
val projectVersionName: String by project
val projectVersionNameSuffix: String by project
val projectVersionNameSuffix = projectVersionName.substringAfter("-", "")
android {
namespace = "dev.patrickgold.florisboard"
@@ -48,7 +50,6 @@ android {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf(
"-Xallow-result-return-type",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-Xjvm-default=all-compatibility",
)
@@ -59,13 +60,13 @@ android {
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 +100,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 +162,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 {

View File

@@ -0,0 +1,92 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "282a1b421e498fd0e21c055b6a4315e0",
"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, `isRemoteDevice` INTEGER NOT NULL)",
"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
},
{
"fieldPath": "isRemoteDevice",
"columnName": "isRemoteDevice",
"affinity": "INTEGER",
"notNull": true
}
],
"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, '282a1b421e498fd0e21c055b6a4315e0')"
]
}
}

View File

@@ -42,10 +42,11 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
android:enableOnBackInvokedCallback="true"
android:roundIcon="@mipmap/floris_app_icon_round"
android:supportsRtl="true"
android:theme="@style/FlorisAppTheme"
tools:targetApi="s">
tools:targetApi="tiramisu">
<!-- Allow app to be profiled for benchmarking and baseline profile generation -->
<profileable android:shell="true"/>
@@ -86,7 +87,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 +132,7 @@
<!-- Copy to Clipboard Activity -->
<activity
android:name="dev.patrickgold.florisboard.FlorisCopyToClipboardActivity"
android:name="dev.patrickgold.florisboard.ime.clipboard.FlorisCopyToClipboardActivity"
android:theme="@style/FlorisAppTheme.Transparent"
android:exported="true">
<intent-filter>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,8 +31,9 @@ import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.ime.keyboard.SpaceBarMode
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHairStyle
import dev.patrickgold.florisboard.ime.media.emoji.EmojiRecentlyUsedHelper
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHistory
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSkinTone
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSuggestionType
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
@@ -56,6 +57,8 @@ 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
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
@@ -171,6 +174,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 +200,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 +598,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 +621,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 +731,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 +753,32 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
}
}
// Migrate media prefs to emoji prefs
// Keep migration rule until: 0.6 dev cycle
"media__emoji_recently_used" -> {
val emojiValues = entry.rawValue.split(";")
val recent = emojiValues.map {
dev.patrickgold.florisboard.ime.media.emoji.Emoji(it, "", emptyList())
}
val data = EmojiHistory(emptyList(), recent)
entry.transform(key = "emoji__history_data", rawValue = Json.encodeToString(data))
}
"media__emoji_recently_used_max_size" -> {
entry.transform(key = "emoji__history_recent_max_size")
}
"media__emoji_preferred_skin_tone" -> {
entry.transform(
key = "emoji__preferred_skin_tone",
rawValue = entry.rawValue.uppercase(), // keep until: 0.5 dev cycle
)
}
"media__emoji_preferred_hair_style" -> {
entry.transform(
key = "emoji__preferred_hair_style",
rawValue = entry.rawValue.uppercase(), // keep until: 0.5 dev cycle
)
}
// Default: keep entry
else -> entry.keepAsIs()
}

View File

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

View File

@@ -23,6 +23,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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 +31,12 @@ 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.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.navigation.NavController
@@ -46,17 +47,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"),
@@ -76,7 +78,7 @@ class FlorisAppActivity : ComponentActivity() {
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()
@@ -141,19 +143,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 +182,9 @@ class FlorisAppActivity : ComponentActivity() {
modifier = Modifier
//.statusBarsPadding()
.navigationBarsPadding()
.conditional(LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
displayCutoutPadding()
}
.imePadding(),
) {
Routes.AppNavHost(
@@ -188,22 +197,22 @@ class FlorisAppActivity : ComponentActivity() {
}
}
LaunchedEffect(fileImportIntent) {
val intent = fileImportIntent
LaunchedEffect(intentToBeHandled) {
val intent = intentToBeHandled
if (intent != null) {
val data = if (intent.action == Intent.ACTION_VIEW) {
intent.data!!
if (intent.action == Intent.ACTION_VIEW && intent.categories?.contains(Intent.CATEGORY_BROWSABLE) == true) {
navController.handleDeepLink(intent)
} else {
intent.clipData!!.getItemAt(0).uri
val data = if (intent.action == Intent.ACTION_VIEW) {
intent.data!!
} else {
intent.clipData!!.getItemAt(0).uri
}
val workspace = runCatching { cacheManager.readFromUriIntoCache(data) }.getOrNull()
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, workspace?.uuid))
}
val workspace = runCatching { cacheManager.readFromUriIntoCache(data) }.getOrNull()
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, workspace?.uuid))
}
fileImportIntent = null
}
SideEffect {
navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher)
intentToBeHandled = null
}
}
}

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import org.florisboard.lib.android.AndroidVersion
class DebugOnPurposeCrashException : Exception(
"Success! The app crashed purposely to display this beautiful screen we all love :)"
@@ -84,6 +85,13 @@ fun DevtoolsScreen() = FlorisScreen {
summary = stringRes(R.string.devtools__show_spelling_overlay__summary),
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
SwitchPreference(
prefs.devtools.showInlineAutofillOverlay,
title = stringRes(R.string.devtools__show_inline_autofill_overlay__label),
summary = stringRes(R.string.devtools__show_inline_autofill_overlay__summary),
enabledIf = { prefs.devtools.enabled isEqualTo true },
visibleIf = { AndroidVersion.ATLEAST_API30_R },
)
SwitchPreference(
prefs.devtools.showKeyTouchBoundaries,
title = stringRes(R.string.devtools__show_key_touch_boundaries__label),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,11 +28,6 @@ import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardHistoryDao
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardHistoryDatabase
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
import org.florisboard.lib.android.AndroidClipboardManager
import org.florisboard.lib.android.AndroidClipboardManager_OnPrimaryClipChangedListener
import org.florisboard.lib.android.setOrClearPrimaryClip
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -45,6 +40,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.florisboard.lib.android.AndroidClipboardManager
import org.florisboard.lib.android.AndroidClipboardManager_OnPrimaryClipChangedListener
import org.florisboard.lib.android.setOrClearPrimaryClip
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import org.florisboard.lib.kotlin.tryOrNull
import java.io.Closeable
@@ -110,7 +110,9 @@ class ClipboardManager(
val primaryClipFlow = _primaryClipFlow.asStateFlow()
inline var primaryClip
get() = primaryClipFlow.value
private set(v) { _primaryClipFlow.value = v }
private set(v) {
_primaryClipFlow.value = v
}
init {
systemClipboardManager.addPrimaryClipChangedListener(this)
@@ -278,6 +280,9 @@ class ClipboardManager(
}
}
/**
* Clears all unpinned items from the clipboard history
*/
fun clearHistory() {
ioScope.launch {
for (item in history().all) {
@@ -287,6 +292,9 @@ class ClipboardManager(
}
}
/**
* Clears the full clipboard history
*/
fun clearFullHistory() {
ioScope.launch {
for (item in history().all) {
@@ -300,26 +308,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 +341,7 @@ class ClipboardManager(
fun unpinClip(item: ClipboardItem) {
ioScope.launch {
clipHistoryDao?.update(item.copy(isPinned = false))
clipHistoryDao?.update(item.copy(isPinned = false))
}
}

View File

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

View File

@@ -17,6 +17,8 @@
package dev.patrickgold.florisboard.ime.clipboard.provider
import android.content.ClipData
import android.content.ClipDescription.EXTRA_IS_REMOTE_DEVICE
import android.content.ClipDescription.EXTRA_IS_SENSITIVE
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
@@ -24,6 +26,8 @@ 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.ColumnInfo
@@ -39,9 +43,14 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.Update
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 +76,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 +87,10 @@ data class ClipboardItem(
val creationTimestampMs: Long,
val isPinned: Boolean,
val mimeTypes: Array<String>,
@EncodeDefault
val isSensitive: Boolean = false,
@EncodeDefault
val isRemoteDevice: Boolean = false,
) {
companion object {
/**
@@ -113,6 +126,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 +176,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()
}
}
@@ -293,7 +332,7 @@ interface ClipboardHistoryDao {
fun deleteAllUnpinned()
}
@Database(entities = [ClipboardItem::class], version = 2)
@Database(entities = [ClipboardItem::class], version = 3)
@TypeConverters(Converters::class)
abstract class ClipboardHistoryDatabase : RoomDatabase() {
abstract fun clipboardItemDao(): ClipboardHistoryDao

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,226 @@
/*
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.media.emoji
import dev.patrickgold.florisboard.app.AppPrefs
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.jetpref.datastore.model.PreferenceSerializer
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class EmojiHistory(
val pinned: List<@Serializable(with = Emoji.ValueOnlySerializer::class) Emoji>,
val recent: List<@Serializable(with = Emoji.ValueOnlySerializer::class) Emoji>,
) {
fun edit(): Editor {
return Editor(pinned.toMutableList(), recent.toMutableList())
}
data class Editor(
val pinned: MutableList<Emoji>,
val recent: MutableList<Emoji>,
) {
fun build(): EmojiHistory {
return EmojiHistory(pinned.toList(), recent.toList())
}
}
enum class UpdateStrategy(val isAutomatic: Boolean, val isPrepend: Boolean) {
AUTO_SORT_PREPEND(isAutomatic = true, isPrepend = true),
AUTO_SORT_APPEND(isAutomatic = true, isPrepend = false),
MANUAL_SORT_PREPEND(isAutomatic = false, isPrepend = true),
MANUAL_SORT_APPEND(isAutomatic = false, isPrepend = false);
}
object Serializer : PreferenceSerializer<EmojiHistory> {
override fun serialize(value: EmojiHistory): String {
return Json.encodeToString(value)
}
override fun deserialize(value: String): EmojiHistory {
try {
return Json.decodeFromString(value)
} catch (e: Exception) {
flogError { "Failed to deserialize EmojiHistory: $e" }
return Empty
}
}
}
companion object {
val Empty = EmojiHistory(emptyList(), emptyList())
@Suppress("ConstPropertyName")
const val MaxSizeUnlimited: Int = 0
}
}
object EmojiHistoryHelper {
private var emojiGuard = Mutex(locked = false)
suspend fun markEmojiUsed(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedUS = prefs.emoji.historyPinnedUpdateStrategy.get()
val recentUS = prefs.emoji.historyRecentUpdateStrategy.get()
val pinnedMaxSize = prefs.emoji.historyPinnedMaxSize.get().let { maxSize ->
if (maxSize == EmojiHistory.MaxSizeUnlimited) Int.MAX_VALUE else maxSize
}
val recentMaxSize = prefs.emoji.historyRecentMaxSize.get().let { maxSize ->
if (maxSize == EmojiHistory.MaxSizeUnlimited) Int.MAX_VALUE else maxSize
}
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
if (pinnedUS.isAutomatic) {
dataMut.pinned.removeAt(pinnedIndex)
dataMut.pinned.addWithStrategy(pinnedUS, emoji)
} else {
// manual sort, keep item in place
}
} else {
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
if (recentUS.isAutomatic) {
dataMut.recent.removeAt(recentIndex)
dataMut.recent.addWithStrategy(recentUS, emoji)
} else {
// manual sort, keep item in place
}
} else {
dataMut.recent.addWithStrategy(recentUS, emoji)
}
}
prefs.emoji.historyData.set(
EmojiHistory(
pinned = dataMut.pinned.takeWithStrategy(pinnedUS, pinnedMaxSize),
recent = dataMut.recent.takeWithStrategy(recentUS, recentMaxSize),
)
)
}
suspend fun pinEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedUS = prefs.emoji.historyPinnedUpdateStrategy.get()
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
dataMut.recent.removeAt(recentIndex)
dataMut.pinned.addWithStrategy(pinnedUS, emoji)
}
prefs.emoji.historyData.set(dataMut.build())
}
suspend fun unpinEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val recentUS = prefs.emoji.historyRecentUpdateStrategy.get()
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
dataMut.pinned.removeAt(pinnedIndex)
dataMut.recent.addWithStrategy(recentUS, emoji)
}
prefs.emoji.historyData.set(dataMut.build())
}
suspend fun moveEmoji(prefs: AppPrefs, emoji: Emoji, offset: Int) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get() || offset == 0) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
dataMut.pinned.move(pinnedIndex, offset)
} else {
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
dataMut.recent.move(recentIndex, offset)
}
}
prefs.emoji.historyData.set(dataMut.build())
}
suspend fun removeEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
dataMut.pinned.removeAt(pinnedIndex)
} else {
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
dataMut.recent.removeAt(recentIndex)
}
}
prefs.emoji.historyData.set(dataMut.build())
}
private fun MutableList<Emoji>.addWithStrategy(strategy: EmojiHistory.UpdateStrategy, emoji: Emoji) {
if (strategy.isPrepend) {
add(0, emoji)
} else {
add(emoji)
}
}
private fun MutableList<Emoji>.takeWithStrategy(
strategy: EmojiHistory.UpdateStrategy,
n: Int,
): List<Emoji> {
return if (strategy.isPrepend) {
take(n)
} else {
takeLast(n)
}
}
private fun MutableList<Emoji>.move(itemIndex: Int, offset: Int) {
val newIndex = (itemIndex + offset).coerceIn(0..<size)
val item = removeAt(itemIndex)
if (newIndex == size) {
add(item)
} else {
add(newIndex, item)
}
}
}

View File

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

View File

@@ -1,58 +0,0 @@
/*
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.media.emoji
import dev.patrickgold.florisboard.app.AppPrefs
import dev.patrickgold.jetpref.datastore.model.PreferenceSerializer
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object EmojiRecentlyUsedHelper {
private const val DELIMITER = ";"
private var emojiGuard = Mutex(locked = false)
suspend fun addEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
val maxSize = prefs.media.emojiRecentlyUsedMaxSize.get()
val list = prefs.media.emojiRecentlyUsed.get().toMutableList()
list.add(0, emoji)
if (maxSize > 0) {
while (list.size > maxSize) {
list.removeLast()
}
}
prefs.media.emojiRecentlyUsed.set(list.distinctBy { it.value })
}
suspend fun removeEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
val list = prefs.media.emojiRecentlyUsed.get().toMutableList()
list.remove(emoji)
prefs.media.emojiRecentlyUsed.set(list.distinctBy { it.value })
}
object Serializer : PreferenceSerializer<List<Emoji>> {
override fun serialize(value: List<Emoji>): String {
return value.joinToString(DELIMITER) { it.value }
}
override fun deserialize(value: String): List<Emoji> {
return value.split(DELIMITER).mapNotNull { rawValue ->
rawValue.trim().let { if (it.isBlank()) null else Emoji(it.trim(), "", emptyList()) }
}
}
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.media.emoji
import kotlinx.coroutines.Dispatchers
@@ -13,10 +29,6 @@ import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider
import dev.patrickgold.florisboard.lib.FlorisLocale
import io.github.reactivecircus.cache4k.Cache
const val EMOJI_SUGGESTION_INDICATOR = ':'
const val EMOJI_SUGGESTION_MAX_COUNT = 5
private const val EMOJI_SUGGESTION_QUERY_MIN_LENGTH = 3
/**
* Provides emoji suggestions within a text input context.
*
@@ -30,7 +42,7 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
override val providerId = "org.florisboard.nlp.providers.emoji"
private val prefs by florisPreferenceModel()
private val lettersRegex = "^:[A-Za-z]*$".toRegex()
private val lettersRegex = "^[A-Za-z]*$".toRegex()
private val cachedEmojiMappings = Cache.Builder().build<FlorisLocale, EmojiDataBySkinTone>()
@@ -52,7 +64,8 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
allowPossiblyOffensive: Boolean,
isPrivateSession: Boolean
): List<SuggestionCandidate> {
val preferredSkinTone = prefs.media.emojiPreferredSkinTone.get()
val preferredSkinTone = prefs.emoji.preferredSkinTone.get()
val showName = prefs.emoji.suggestionCandidateShowName.get()
val query = validateInputQuery(content.composingText) ?: return emptyList()
val emojis = cachedEmojiMappings.get(subtype.primaryLocale)?.get(preferredSkinTone) ?: emptyList()
val candidates = withContext(Dispatchers.Default) {
@@ -62,14 +75,24 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
emoji.keywords.any { it.contains(query, ignoreCase = true) }
}
.limit(maxCandidateCount.toLong())
.map { EmojiSuggestionCandidate(it) }
.map { emoji ->
EmojiSuggestionCandidate(
emoji = emoji,
showName = showName,
sourceProvider = this@EmojiSuggestionProvider,
)
}
.collect(Collectors.toList())
}
return candidates
}
override suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) {
// No-op
val updateHistory = prefs.emoji.suggestionUpdateHistory.get()
if (!updateHistory || candidate !is EmojiSuggestionCandidate) {
return
}
EmojiHistoryHelper.markEmojiUsed(prefs, candidate.emoji)
}
override suspend fun notifySuggestionReverted(subtype: Subtype, candidate: SuggestionCandidate) {
@@ -90,15 +113,18 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
* Validates the user input query for emoji suggestions.
*/
private fun validateInputQuery(composingText: CharSequence): String? {
if (!composingText.startsWith(EMOJI_SUGGESTION_INDICATOR)) {
val prefix = prefs.emoji.suggestionType.get().prefix
val queryMinLength = prefs.emoji.suggestionQueryMinLength.get() + prefix.length
if (prefix.isNotEmpty() && !composingText.startsWith(prefix)) {
return null
}
if (composingText.length <= EMOJI_SUGGESTION_QUERY_MIN_LENGTH) {
if (composingText.length < queryMinLength) {
return null
}
if (!lettersRegex.matches(composingText)) {
val emojiPartialName = composingText.substring(prefix.length)
if (!lettersRegex.matches(emojiPartialName)) {
return null
}
return composingText.substring(1)
return emojiPartialName
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.media.emoji
enum class EmojiSuggestionType(val prefix: String) {
LEADING_COLON(":"),
INLINE_TEXT(""),
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
import android.content.Context
import android.os.Build
import android.util.Size
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestion
import android.view.inputmethod.InlineSuggestionInfo
import android.widget.inline.InlineContentView
import androidx.annotation.RequiresApi
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.lib.devtools.flogInfo
import dev.patrickgold.florisboard.lib.devtools.flogWarning
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
data class NlpInlineAutofillSuggestion(
val info: InlineSuggestionInfo,
val view: InlineContentView?,
)
object NlpInlineAutofill {
private val currentSequenceId = AtomicInteger(0)
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val setterGuard = Mutex()
private val _suggestions = MutableStateFlow<List<NlpInlineAutofillSuggestion>>(emptyList())
val suggestions = _suggestions
@RequiresApi(Build.VERSION_CODES.R)
fun showInlineSuggestions(context: Context, rawSuggestions: List<InlineSuggestion>): Boolean {
val sequenceId = generateSequenceId()
if (rawSuggestions.isEmpty()) {
clearInlineSuggestions(sequenceId)
return false
}
scope.launch {
val size = Size(ViewGroup.LayoutParams.WRAP_CONTENT, FlorisImeSizing.Static.smartbarHeightPx)
val latch = CountDownLatch(rawSuggestions.size)
val suggestionsArray = Array<NlpInlineAutofillSuggestion?>(rawSuggestions.size) { null }
flogInfo { "showInlineSuggestions: [${sequenceId}] start inflating suggestions" }
for ((index, rawSuggestion) in rawSuggestions.withIndex()) {
rawSuggestion.inflate(context, size, context.mainExecutor) { view ->
suggestionsArray[index] = NlpInlineAutofillSuggestion(rawSuggestion.info, view)
latch.countDown()
}
}
if (!latch.await(2_000, TimeUnit.MILLISECONDS)) {
flogWarning { "showInlineSuggestions: [${sequenceId}] timed out while waiting for all " +
"suggestions to inflate" }
return@launch
}
val suggestions = suggestionsArray.filterNotNull().sortedByDescending { it.info.isPinned }
setterGuard.lock()
flogInfo { "showInlineSuggestions: [${sequenceId}] successfully inflated " +
"${suggestions.count { it.view != null }} out of ${suggestions.size} suggestions" }
if (currentSequenceId.get() == sequenceId) {
flogInfo { "showInlineSuggestions: [${sequenceId}] setting suggestions" }
_suggestions.value = suggestions
} else {
flogWarning { "showInlineSuggestions: [${sequenceId}] seqId != current, skip setting suggestions" }
}
setterGuard.unlock()
}
return true
}
fun clearInlineSuggestions() {
// Increment sequence id to invalidate eventual pending suggestions
clearInlineSuggestions(generateSequenceId())
}
private fun clearInlineSuggestions(sequenceId: Int) {
scope.launch {
setterGuard.lock()
flogInfo { "clearInlineSuggestions: [${sequenceId}] clearing suggestions" }
_suggestions.value = emptyList()
setterGuard.unlock()
}
}
private fun generateSequenceId(): Int {
return currentSequenceId.incrementAndGet()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,8 +22,8 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.ZoomOutMap
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -81,9 +81,9 @@ fun RowScope.OneHandedPanel(
) {
Icon(
imageVector = if (panelSide == OneHandedMode.START) {
Icons.Default.KeyboardArrowLeft
Icons.AutoMirrored.Filled.KeyboardArrowLeft
} else {
Icons.Default.KeyboardArrowRight
Icons.AutoMirrored.Filled.KeyboardArrowRight
},
contentDescription = stringRes(
if (panelSide == OneHandedMode.START) {

View File

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

View File

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

View File

@@ -1,57 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.smartbar
import android.os.Build
import android.util.Size
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestion
import android.widget.inline.InlineContentView
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.viewinterop.AndroidView
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.nlpManager
@RequiresApi(Build.VERSION_CODES.R)
@Composable
fun InlineSuggestionView(inlineSuggestion: InlineSuggestion) = with(LocalDensity.current) {
val context = LocalContext.current
val nlpManager by context.nlpManager()
val size = Size(ViewGroup.LayoutParams.WRAP_CONTENT, FlorisImeSizing.smartbarHeight.toPx().toInt())
var inlineContentView by remember { mutableStateOf<InlineContentView?>(null) }
LaunchedEffect(Unit) {
nlpManager.inflateOrGet(context, size, inlineSuggestion) { view ->
inlineContentView = view
}
}
if (inlineContentView != null) {
AndroidView(
factory = { inlineContentView!! },
)
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.smartbar
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.viewinterop.AndroidView
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofillSuggestion
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.toIntOffset
@RequiresApi(Build.VERSION_CODES.R)
@Composable
fun InlineSuggestionsUi(
inlineSuggestions: List<NlpInlineAutofillSuggestion>,
modifier: Modifier = Modifier,
) {
val scrollState = rememberScrollState()
val almostEmptyRect = remember { android.graphics.Rect(0, 0, 1, 1) }
Row(
modifier
.fillMaxSize()
.florisHorizontalScroll(
state = scrollState,
scrollbarHeight = CandidatesRowScrollbarHeight,
),
) {
val xMin = scrollState.value
val xMax = scrollState.value + scrollState.viewportSize
for (inlineSuggestion in inlineSuggestions) {
if (inlineSuggestion.view == null) {
continue
}
var chipPos by remember { mutableStateOf(IntOffset.Zero) }
AndroidView(
modifier = Modifier.onGloballyPositioned { chipPos = it.positionInParent().toIntOffset() },
factory = { inlineSuggestion.view },
update = { view ->
view.clipBounds = android.graphics.Rect(
(xMin - chipPos.x).coerceAtLeast(0),
0,
(xMax - chipPos.x).coerceAtMost(view.width),
view.height,
)
// The empty rect is a workaround for a bug (I suppose) where an empty rect causes
// no clipping, but we actually want to completely hide the view.
// Thus we just show the topmost pixel of the view, which due to the round shape
// of the theme should be transparent anyways.
if (view.clipBounds.isEmpty) {
view.clipBounds = almostEmptyRect
}
}
)
}
}
}

View File

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

View File

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

View File

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

View File

@@ -220,31 +220,39 @@ 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, isSmartbarKeyboard, keyboardWidth, keyboardHeight, keyMarginH, keyMarginV,
keyboardRowBaseHeight
) {
TextKey(data = TextKeyData.UNSPECIFIED).also { desiredKey ->
desiredKey.touchBounds.apply {
if (isSmartbarKeyboard) {
width = keyboardWidth / 8f
height = keyboardHeight
} else {
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, !isSmartbarKeyboard)
}
}
desiredKey.visibleBounds.applyFrom(desiredKey.touchBounds).deflateBy(keyMarginH, keyMarginV)
keyboard.layout(keyboardWidth, keyboardHeight, desiredKey, !isSmartbarKeyboard)
val fontSizeMultiplier = prefs.keyboard.fontSizeMultiplier()
val popupUiController = rememberPopupUiController(

View File

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

View File

@@ -199,70 +199,68 @@ 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 singleIconChipStyle = ViewStyle.Builder().run {
setBackground(bgDrawable)
setPadding(0, 0, 0, 0)
build()
}
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(singleIconChipStyle)
setChipStyle(chipStyle)
setStartIconStyle(iconStyle)
setEndIconStyle(iconStyle)
setTitleStyle(titleStyle)
setSubtitleStyle(subtitleStyle)
build()
}
return UiVersions.newStylesBuilder().run {
addStyle(suggestionStyle)
build()
}
}
private fun getColorFromThemeAttribute(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.lib.compose
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
fun LazyGridScope.header(
key: Any? = null,
content: @Composable LazyGridItemScope.() -> Unit,
) {
item(key, span = { GridItemSpan(this.maxLineSpan) }, content = content)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,9 +13,24 @@
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">الرموز التعبيرية</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">وجوه تعبيرية</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">كاوموجي</string>
<string name="prefs__media__emoji_recently_used_max_size">الحد الأقصى لحجم السجل للرموز التعبيرية</string>
<string name="prefs__media__emoji_preferred_skin_tone">لون الرموز التعبيرية المفضل</string>
<string name="prefs__media__emoji_preferred_hair_style">نوع الشعر المفضل للرموز التعبيرية</string>
<string name="prefs__media__emoji_history__title" comment="Preference group title">سجل الرموز التعبيرية</string>
<string name="prefs__media__emoji_history_enabled" comment="Preference title">تمكين سجل الرموز التعبيرية</string>
<string name="prefs__media__emoji_history_enabled__summary" comment="Preference summary">الاحتفاظ بالرموز التعبيرية المستخدمة مؤخرًا للوصول السريع</string>
<string name="prefs__media__emoji_history_pinned_update_strategy" comment="Preference title">تحديث الاستراتيجية (مثبت)</string>
<string name="prefs__media__emoji_history_recent_update_strategy" comment="Preference title">إستراتيجية التحديث (الأخيرة)</string>
<string name="prefs__media__emoji_history_max_size">الحد الأقصى للعناصر التي يجب الاحتفاظ بها</string>
<string name="prefs__media__emoji_suggestion__title" comment="Preference group title">اقتراحات الرموز التعبيرية</string>
<string name="prefs__media__emoji_suggestion_enabled" comment="Preference title">تمكين اقتراحات الرموز التعبيرية</string>
<string name="prefs__media__emoji_suggestion_enabled__summary" comment="Preference summary">تقديم اقتراحات الرموز التعبيرية أثناء الكتابة</string>
<string name="prefs__media__emoji_suggestion_type" comment="Preference title">نوع التفعيل</string>
<string name="prefs__media__emoji_suggestion_update_history" comment="Preference title">تحديث سجل الرموز التعبيرية</string>
<string name="prefs__media__emoji_suggestion_update_history__summary" comment="Preference summary">قَبُول الرموز التعبيرية المقترحة يضيفها إلى تاريخ الرموز التعبيرية</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name" comment="Preference title">عرض اسم الرمز التعبيري</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name__summary" comment="Preference summary">تعرض اقتراحات الرموز التعبيرية اسمها جنبًا إلى جنب مع الرموز التعبيرية</string>
<string name="prefs__media__emoji_suggestion_query_min_length" comment="Preference title">الحد الأدنى لطول الاستعلام</string>
<string name="prefs__media__emoji_suggestion_candidate_max_count" comment="Preference title">الحد الأقصى لعدد المرشحين</string>
<!-- Emoji strings -->
<string name="emoji__category__smileys_emotion" comment="Emoji category name">وجوه تعبيرية و عواطف</string>
<string name="emoji__category__people_body" comment="Emoji category name">أشخاص و أجسام</string>
@@ -26,10 +41,12 @@
<string name="emoji__category__objects" comment="Emoji category name">أشياء</string>
<string name="emoji__category__symbols" comment="Emoji category name">الرموز</string>
<string name="emoji__category__flags" comment="Emoji category name">أعلام</string>
<string name="emoji__recently_used__empty_message" comment="Message if no recently used emojis exist">لم يتم العثور على رموز تعبيرية مستخدمة مؤخرًا. بمجرد أن تبدأ في استخدامها الرموز التعبيرية فإنها ستظهر تلقائيًا هنا.</string>
<string name="emoji__recently_used__phone_locked_message" comment="Message to show if phone is locked">للوصول إلى سجل الرموز التعبيرية الخاص بك، يرجى أولاً إلغاء قُفْل جهازك.</string>
<string name="emoji__recently_used__removal_tip" comment="Feature discoverability for removal of recently used emojis">نصيحة للمحترفين: الضغط لفترة طويلة على الرموز التعبيرية المستخدمة مؤخرًا لإزالتها من هذا العرض مرة أخرى!</string>
<string name="emoji__recently_used__removal_success_message" comment="Toast message if user has long pressed emoji in recently used collection to remove it">تمت إزالة {emoji} من الرموز التعبيرية المستخدمة مؤخرًا</string>
<string name="emoji__history__empty_message" comment="Message if the emoji history is empty">لم يتم العثور على رموز تعبيرية مستخدمة مؤخرًا. بمجرد بَدْء كتابة الرموز التعبيرية، ستظهر تلقائيًا هنا.</string>
<string name="emoji__history__phone_locked_message" comment="Message to show if phone is locked">للوصول إلى سجل الرموز التعبيرية الخاص بك، يرجى أولاً إلغاء قُفْل جهازك.</string>
<string name="emoji__history__usage_tip" comment="Feature discoverability for actions of emoji history">نصيحة احترافية: اضغط لفترة طويلة على الرموز التعبيرية في سجل الرموز التعبيرية لتثبيتها أو إزالتها!</string>
<string name="emoji__history__removal_success_message" comment="Toast message if user has used the delete action on an emoji in the emoji history">تمت إزالة {emoji} من سجل الرموز التعبيرية</string>
<string name="emoji__history__pinned">مثبّت</string>
<string name="emoji__history__recent">الحديثة</string>
<!-- Quick action strings -->
<string name="quick_action__arrow_up" maxLength="12">سهم للأعلى</string>
<string name="quick_action__arrow_up__tooltip">تنفيذ السهم للأعلى</string>
@@ -83,7 +100,7 @@
<string name="incognito_mode__toast_after_disabled">الوضع الخاص هو الان معطل افتراضيا</string>
<!-- Settings UI strings -->
<string name="settings__title" comment="Title of Settings">الإعدادات</string>
<string name="settings__preview_keyboard" comment="Hint for try your setup box">قم بتجربة اعداداتك</string>
<string name="settings__preview_keyboard" comment="Hint for try your setup box">قم بتجربة أعداداتك</string>
<string name="settings__help" comment="General label for help buttons in Settings">مساعدة</string>
<string name="settings__default" comment="General string which is used when a preference has the default value set">الإفتراضي</string>
<string name="settings__system_default" comment="General string which is used when a preference has the system default value set">الإعداد الافتراضي</string>
@@ -124,21 +141,20 @@
<string name="settings__localization__subtype_error_fields_no_value" comment="Error message shown in subtype editor if at least one field is set to '- select -' (means no value specified)">حقل واحد على الأقل ليس له قيمة محددة. الرجاء اختيار قيمة للحقل (الحقول).</string>
<string name="settings__localization__subtype_error_layout_not_installed" comment="Error message shown in subtype list when a layout is not installed, where %s will be replaced by the layout ID">{layout_id} (غير مثبت)</string>
<string name="settings__localization__group_layouts__label" comment="Label of layouts group">التخطيطات</string>
<string name="settings__localization__subtype_delete_confirmation_title" comment="Title of the subtype delete confirmation dialog">تأكيد الحذف</string>
<string name="settings__localization__subtype_delete_confirmation_warning" comment="Warning message in the confirmation dialog to confirm the user's intent to delete">هل أنت متيقِّن أنك تريد حذف النوع الفرعي؟</string>
<string name="settings__theme__title" comment="Title of the Theme screen">المظهر</string>
<string name="pref__theme__mode__label" comment="Label of the theme mode preference">وضع الثيم</string>
<string name="pref__theme__sunrise_time__label" comment="Label of the sunrise time preference">وقت الشروق</string>
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">وقت الغروب</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">نسق نهاري</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">نسق ليلي</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">النسق المحدد</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">تكييف الألوان مع التطبيق</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">تتكيف ألوان الثيم مع تلك الموجودة في التطبيق الحالي ، إذا كان التطبيق المستهدف يدعم ذلك.</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">أدارة السمات المثبتة</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">أصول تطبيق FlorisBoard</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">الذاكرة الداخلية</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">مزود خارجي</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">اختيار النمط الصباحي</string>
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">اختيار النمط الليلي</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">أدارة السمات المثبتة</string>
<string name="settings__theme_editor__fine_tune__title">تعديل متقدم</string>
<string name="settings__theme_editor__fine_tune__level">مستوى التعديل</string>
<string name="settings__theme_editor__fine_tune__display_colors_as">عرض اللون كـ</string>
@@ -311,6 +327,7 @@
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">مدة الضغط المطوّل على المفتاح</string>
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">مفتاح المسافة يبدل إلى الأحرف</string>
<string name="pref__keyboard__space_bar_switches_to_characters__summary" comment="Preference summary">يعود تلقائيًا إلى الأحرف عندما تكون في الرموز أو الأرقام</string>
<string name="pref__keyboard__incognito_indicator__label" comment="Preference title">علامة وضع التخفي</string>
<!-- Smartbar strings -->
<string name="settings__smartbar__title" comment="Title of Smartbar screen">الشريط الذكـي</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">تفعيل الشريط الذكي</string>
@@ -413,6 +430,7 @@
<string name="pref__advanced__settings_theme__light" comment="Possible value of Settings theme preference in Advanced">فاتح</string>
<string name="pref__advanced__settings_theme__dark" comment="Possible value of Settings theme preference in Advanced">داكن</string>
<string name="pref__advanced__settings_theme__amoled_dark" comment="Possible value of Settings theme preference in Advanced">أسود قاتم</string>
<string name="pref__advanced__settings_material_you__label" comment="Label of Material You preference in Advanced">استخدام تصميم Material You</string>
<string name="pref__advanced__settings_language__label" comment="Label of Settings language preference in Advanced">إعدادات اللغة</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">إظهار أيقونة البرنامج في درج التطبيقات</string>
<string name="pref__advanced__show_app_icon__summary_atleast_q" comment="Summary of Show app icon preference in Advanced for Android 10+">ممكّن دائمًا على اندرويد 10+ فما فوق بسبب قيود النظام</string>
@@ -471,6 +489,10 @@
<string name="backup_and_restore__back_up__files_ime_keyboard">ملحقات لوحة المفاتيح</string>
<string name="backup_and_restore__back_up__files_ime_spelling">ملحقات التهجئة/ القاموس</string>
<string name="backup_and_restore__back_up__files_ime_theme">ملحقات السمات</string>
<string name="backup_and_restore__back_up__files_clipboard_history">سجل الحافظة</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_text_items">عناصر النص</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_image_items">صور</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_video_items">فيديوهات</string>
<string name="backup_and_restore__back_up__success">تم تصدير أرشيف النسخة الاحتياطية بنجاح!</string>
<string name="backup_and_restore__back_up__failure">فشل في تصدير أرشيف الاضافات: {error_message}</string>
<string name="backup_and_restore__restore__title">إستعادة البيانات</string>
@@ -558,6 +580,8 @@
<string name="devtools__show_input_state_overlay__summary" comment="Summary of Show input cache overlay in Devtools">إظهار حالة تراكب الإدخال لأغراض التطوير</string>
<string name="devtools__show_spelling_overlay__label" comment="Label of Show spelling overlay in Devtools">أظهر طبقات التهجئة</string>
<string name="devtools__show_spelling_overlay__summary" comment="Summary of Show spelling overlay in Devtools">إظهار نتائج تراكب التهجئة لأغراض التطوير</string>
<string name="devtools__show_inline_autofill_overlay__label">عرض تراكب الملء التلقائي المضمن</string>
<string name="devtools__show_inline_autofill_overlay__summary">تراكب مع نتائج الملء التلقائي المضمنة الحالية للتصحيح</string>
<string name="devtools__show_key_touch_boundaries__label" comment="Label of Show key touch boundaries in Devtools">عرض حدود لمس الازرار</string>
<string name="devtools__show_key_touch_boundaries__summary" comment="Summary of Show key touch boundaries in Devtools">رسم حد خارجي للمس الازرار باللون الاحمر</string>
<string name="devtools__show_drag_and_drop_helpers__label" comment="Label of Show drag and drop helpers in Devtools">إظهار مساعد السحب والإفلات</string>
@@ -573,7 +597,16 @@
<string name="devtools__android_settings_secure__title" comment="Title of Android settings (secure) screen">إعدادات أندرويد الأمنة</string>
<string name="devtools__android_settings_system__title" comment="Title of Android settings (system) screen">إعدادات نظام أندرويد</string>
<string name="devtools__android_locales__title" comment="Title of Android locales screen">لغات النظام</string>
<string name="devtools__debuglog__title">سجل التصحيح</string>
<string name="devtools__debuglog__copied_to_clipboard">تم نسخ سجل التصحيح إلى الحافظة</string>
<string name="devtools__debuglog__copy_log">نسخ السجل</string>
<string name="devtools__debuglog__copy_for_github">نسخ السجل (تنسيق GitHub)</string>
<string name="devtools__debuglog__loading">تحميل…</string>
<!-- Extension strings -->
<string name="ext__home__title">الإضافات والملحقات</string>
<string name="ext__list__ext_theme">ملحقات السمات</string>
<string name="ext__list__ext_keyboard">ملحقات لوحة المفاتيح</string>
<string name="ext__list__ext_languagepack">ملحقات حزم اللغة</string>
<string name="ext__meta__authors">المؤلفون</string>
<string name="ext__meta__components">العناصر المجمعة</string>
<string name="ext__meta__components_theme">الثيمات المجمعة</string>
@@ -626,6 +659,40 @@
<string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">من المتوقع وجود ملف وسائط (صورة ، صوت ، خط ، إلخ) ولكن تم العثور على أرشيف ملحق.</string>
<string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">من المتوقع وجود أرشيف ملحق ولكن تم العثور على ملف وسائط (صورة ، صوت ، خط ، إلخ).</string>
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">حدث خطأ غير متوقع أثناء الاستيراد. تم توفير التفاصيل التالية:</string>
<string name="ext__validation__enter_package_name">يرجى إدخال اسم الحُزْمَة</string>
<string name="ext__validation__error_package_name">اسم الحُزْمَة لا يتطابق مع المدى {id_regex}</string>
<string name="ext__validation__enter_version">الرجاء إدخال إصدار</string>
<string name="ext__validation__enter_title">الرجاء إدخال عنوان</string>
<string name="ext__validation__enter_maintainer">يرجى إدخال مشرف صيانة صالح واحد على الأقل</string>
<string name="ext__validation__enter_license">يرجى إدخال معرف الترخيص</string>
<string name="ext__validation__enter_component_id">يرجى إدخال معرّف المكون</string>
<string name="ext__validation__error_component_id">يرجى إدخال معرف مكون مطابق {component_id_regex}</string>
<string name="ext__validation__enter_component_label">يرجى إدخال تسمية المكون</string>
<string name="ext__validation__hint_component_label_to_long">عنوان المكون الخاص بك طويل جدًا، مما قد يؤدي إلى أقتصاصه في واجهة المستخدم</string>
<string name="ext__validation__error_author">يرجى إدخال منشئ صالح واحد على الأقل</string>
<string name="ext__validation__error_stylesheet_path_blank">يجب ألا يكون مسار ورقة الأنماط فارغًا</string>
<string name="ext__validation__error_stylesheet_path">يرجى إدخال مسار ورقة أنماط صالح يطابق {stylesheet_path_regex}</string>
<string name="ext__validation__enter_property">يرجى إدخال اسم المتغير</string>
<string name="ext__validation__error_property">يرجى إدخال اسم متغير صالح يطابق {variable_name_regex}</string>
<string name="ext__validation__hint_property" tools:ignore="TypographyDashes">حسب الاتفاقية، يبدأ اسم متغير FlorisCSS بشرطتين (--)</string>
<string name="ext__validation__enter_color">يرجى إدخال سلسلة ألوان</string>
<string name="ext__validation__error_color">يرجى إدخال سلسلة ألوان صالحة</string>
<string name="ext__validation__enter_dp_size">يرجى إدخال حجم dp</string>
<string name="ext__validation__enter_valid_number">يرجى إدخال رقم صحيح</string>
<string name="ext__validation__enter_positive_number">يرجى إدخال رَقْم موجب (&gt;=0)</string>
<string name="ext__validation__enter_percent_size">يرجى إدخال النسبة المئوية للحجم</string>
<string name="ext__validation__enter_number_between_0_100">يرجى إدخال رَقْم موجب بين 0 و 100</string>
<string name="ext__validation__hint_value_above_50_percent">أي قيمة أعلى من 50 ٪ ستتصرف كما لو أنك قمت بتعيين 50 ٪، فكر في خفض النسبة المئوية لحجمك</string>
<string name="ext__update_box__internet_permission_hint">نظرًا لأن هذا التطبيق ليس لديه إذن إنترنت، يجب التحقق من تحديثات الإضافات المثبتة يدويًا.</string>
<string name="ext__update_box__search_for_updates">البحث عن التحديثات</string>
<string name="ext__addon_management_box__managing_placeholder">إدارة {extensions}</string>
<string name="ext__addon_management_box__addon_manager_info">يمكن التعامل مع جميع المهام المتعلقة باستيراد الملحقات وتصديرها وإنشائها وتخصيصها وإزالتها بواسطة مدير الإضافات المركزي.</string>
<string name="ext__addon_management_box__go_to_page">انتقل إلى {ext_home_title}</string>
<string name="ext__home__info">يمكنك تنزيل الإضافات وتثبيتها من متجر إضافات FlorisBoard أو استيراد أي مِلَفّ ملحق قمت بتنزيله من الإنترنت.</string>
<string name="ext__home__visit_store">زيارة متجر الإضافات</string>
<string name="ext__home__manage_extensions">إدارة الإضافات المثبتة</string>
<string name="ext__list__view_details">عرض التفاصيل</string>
<string name="ext__check_updates__title">التحقق من وجود تحديثات</string>
<!-- Action strings -->
<string name="action__add">إضافة</string>
<string name="action__apply">تطبيق</string>
@@ -693,6 +760,14 @@
<string name="enum__display_language_names_in__system_locale__description" comment="Enum value description">يتم عرض أسماء اللغات عبر واجهة مستخدم التطبيق ولوحة المفاتيح باللغة المحلية التي تم تعيينها للجهاز بأكمله</string>
<string name="enum__display_language_names_in__native_locale" comment="Enum value label">اللغة الأصلية</string>
<string name="enum__display_language_names_in__native_locale__description" comment="Enum value description">يتم عرض أسماء اللغات عبر واجهة مستخدم التطبيق ولوحة المفاتيح باللغة المحلية المشار إليها بنفسها</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend" comment="Enum value label">الفرز التلقائي (الإعداد المسبق)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend__description" comment="Enum value description">إعادة ترتيب الرموز التعبيرية تلقائيًا عند استخدام الرموز التعبيرية. تمت إضافة رموز تعبيرية جديدة إلى البداية.</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append" comment="Enum value label">الفرز التلقائي (إلحاق)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append__description" comment="Enum value description">إعادة ترتيب الرموز التعبيرية تلقائيًا عند استخدام الرموز التعبيرية. تتم إضافة رموز تعبيرية جديدة إلى النهاية.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend" comment="Enum value label">الفرز اليدوي (التمهيدي)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend__description" comment="Enum value description">لا تقم بإعادة جلب الرموز التعبيرية تلقائيًا عند استخدام الرموز التعبيرية. تمت إضافة رموز تعبيرية جديدة إلى البداية.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append" comment="Enum value label">الفرز اليدوي (إلحاق)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append__description" comment="Enum value description">لا تقم بإعادة ترتيب الرموز التعبيرية تلقائيًا عند استخدام الرموز التعبيرية. تتم إضافة رموز تعبيرية جديدة إلى النهاية.</string>
<string name="enum__emoji_skin_tone__default" comment="Enum value label">{emoji} اللون الافتراضي للبشرة</string>
<string name="enum__emoji_skin_tone__light_skin_tone" comment="Enum value label">{emoji} اللون الفاتح للبشرة</string>
<string name="enum__emoji_skin_tone__medium_light_skin_tone" comment="Enum value label">{emoji} اللون الفاتح المتوسط للبشرة</string>
@@ -704,6 +779,10 @@
<string name="enum__emoji_hair_style__curly_hair" comment="Enum value label">{emoji} شعر مجعد</string>
<string name="enum__emoji_hair_style__white_hair" comment="Enum value label">{emoji} شعر ابيض</string>
<string name="enum__emoji_hair_style__bald" comment="Enum value label">{emoji} اصلع</string>
<string name="enum__emoji_suggestion_type__leading_colon">فاصلة متقدمة</string>
<string name="enum__emoji_suggestion_type__leading_colon__description" comment="Keep the :emoji_name while translating, this is a syntax guide">اقترح الرموز التعبيرية باستخدام بناء جملة:emoji_name</string>
<string name="enum__emoji_suggestion_type__inline_text">نص المعلومات المضمنة</string>
<string name="enum__emoji_suggestion_type__inline_text__description">اقترح الرموز التعبيرية ببساطة عن طريق كتابة اسم الرموز التعبيرية ككلمة</string>
<string name="enum__extended_actions_placement__above_candidates" comment="Enum value label">تجاهل المرشح</string>
<string name="enum__extended_actions_placement__above_candidates__description" comment="Enum value description">يضع صف الإجراءات الموسعة بين واجهة مستخدم التطبيق والصف المرشح</string>
<string name="enum__extended_actions_placement__below_candidates" comment="Enum value label">تحت المرشحون</string>
@@ -720,6 +799,8 @@
<string name="enum__key_hint_mode__hint_priority__description" comment="Enum value description">دائمًا ما يكون الحرف الأولي المحدد بعد الضغط لفترة طويلة هو رمز التلميح ، أو العلامة الأساسية في حالة عدم توفر رمز تلميح</string>
<string name="enum__key_hint_mode__smart_priority" comment="Enum value label">تحديد الاولويات الذكي</string>
<string name="enum__key_hint_mode__smart_priority__description" comment="Enum value description">يتم تحديد الحرف الأولي المحدد بعد الضغط لفترة طويلة ديناميكيًا ليكون إما العلامة الأساسية أو رمز التلميح ، بناءً على اللغة والتخطيط الحاليين</string>
<string name="enum__incognito_display_mode__replace_shared_actions_toggle" comment="Enum value label">استبدل أيقونة تبديل الإجراءات المشتركة بمؤشر التصفح المتخفي</string>
<string name="enum__incognito_display_mode__display_behind_keyboard" comment="Enum value label">عرض مؤشر التصفح المتخفي خلف لوحة المفاتيح</string>
<string name="enum__incognito_mode__force_off" comment="Enum value label">إيقاف قسري</string>
<string name="enum__incognito_mode__force_off__description" comment="Enum value description">سيتم دائما تعطيل وضع التصفح المتخفي، بغض النظر عن الخيارات التي تم تمريرها في التطبيق المستهدف. لن يتوفر الإجراء السريع للتصفح المتخفي في الشريط الذكي مع هذا الخِيار.</string>
<string name="enum__incognito_mode__force_on" comment="Enum value label">تفعيل قسري</string>
@@ -833,4 +914,20 @@
<item quantity="many">{v} عناصر</item>
<item quantity="other">{v} عناصر</item>
</plurals>
<plurals name="unit__characters__written">
<item quantity="zero">{v} حرف</item>
<item quantity="one">{v} حرف</item>
<item quantity="two">{v} حرفان</item>
<item quantity="few">{v} أحرف</item>
<item quantity="many">{v} احرف</item>
<item quantity="other">{v} احرف</item>
</plurals>
<plurals name="unit__candidates__written">
<item quantity="zero">{v} مرشح</item>
<item quantity="one">{v} مرشح</item>
<item quantity="two">{v} مرشحان</item>
<item quantity="few">{v} مرشحات</item>
<item quantity="many">{v} مرشحات</item>
<item quantity="other">{v} مرشحون</item>
</plurals>
</resources>

View File

@@ -13,6 +13,9 @@
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">Fustaxes</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">Fustaxes ASCII</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">Kaomoji</string>
<string name="prefs__media__emoji_preferred_skin_tone">لون البشرة المفضل للرموز التعبيرية</string>
<string name="prefs__media__emoji_preferred_hair_style">تصفيفة الشعر الرموز التعبيرية المفضلة</string>
<string name="prefs__media__emoji_history_enabled" comment="Preference title">Activar l\'historial de fustaxes</string>
<!-- Emoji strings -->
<string name="emoji__category__smileys_emotion" comment="Emoji category name">Sorrises y fustaxes</string>
<string name="emoji__category__people_body" comment="Emoji category name">Persones y cuerpu</string>
@@ -23,18 +26,20 @@
<string name="emoji__category__objects" comment="Emoji category name">Oxetos</string>
<string name="emoji__category__symbols" comment="Emoji category name">Símbolos</string>
<string name="emoji__category__flags" comment="Emoji category name">Banderes</string>
<string name="emoji__recently_used__empty_message" comment="Message if no recently used emojis exist">Nun s\'atopó nengún fustaxe que s\'usare apocayá. Namás que comiences a usalos, apaecen equí.</string>
<string name="emoji__recently_used__phone_locked_message" comment="Message to show if phone is locked">P\'acceder al historial de fustaxes, desbloquia\'l preséu primero.</string>
<string name="emoji__recently_used__removal_tip" comment="Feature discoverability for removal of recently used emojis">Conseyu: ¡ten primíos los fustaxes usaos apocayá pa volver quitalos d\'esta seición!</string>
<string name="emoji__recently_used__removal_success_message" comment="Toast message if user has long pressed emoji in recently used collection to remove it">Quitóse «{emoji}» de los fustaxes usaos apocayá</string>
<!-- Quick action strings -->
<string name="quick_action__arrow_up" maxLength="12">سهم لأعلى</string>
<string name="quick_action__arrow_up__tooltip">أداء السهم لأعلى</string>
<string name="quick_action__arrow_down" maxLength="12">سهم لأسفل</string>
<string name="quick_action__arrow_down__tooltip">تنفيذ السهم لأسفل</string>
<string name="quick_action__arrow_left" maxLength="12">Flecha esq.</string>
<string name="quick_action__arrow_right" maxLength="12">Flecha der.</string>
<string name="quick_action__clipboard_copy" maxLength="12">Copiar</string>
<string name="quick_action__clipboard_cut" maxLength="12">Cortar</string>
<string name="quick_action__clipboard_paste" maxLength="12">Apegar</string>
<string name="quick_action__ime_ui_mode_clipboard" maxLength="12">Cartafueyu</string>
<string name="quick_action__ime_ui_mode_clipboard__tooltip">Abrir l\'historial de fustaxes</string>
<string name="quick_action__ime_ui_mode_media__tooltip">Abrir el panel de fustaxes</string>
<string name="quick_action__settings" maxLength="12">Axustes</string>
<string name="quick_action__settings" maxLength="12">Opciones</string>
<string name="quick_action__settings__tooltip">Abrir la configuración</string>
<string name="quick_action__undo" maxLength="12">Desfacer</string>
<string name="quick_action__redo" maxLength="12">Refacer</string>
@@ -45,12 +50,14 @@
<!-- Incognito mode strings -->
<!-- Settings UI strings -->
<string name="settings__title" comment="Title of Settings">Configuración</string>
<string name="settings__preview_keyboard" comment="Hint for try your setup box">Toca equí pa probar la configuración</string>
<string name="settings__help" comment="General label for help buttons in Settings">Ayuda</string>
<string name="settings__default" comment="General string which is used when a preference has the default value set">Por defeutu</string>
<string name="settings__home__title" comment="Title of the Home screen">Afáyate en {app_name}</string>
<string name="settings__localization__title" comment="Title of languages and Layout screen">Llingües y distribuciones</string>
<string name="settings__localization__display_language_names_in__label" comment="Label of Display language names in preference">Amosar los nomes de les llingües na</string>
<string name="settings__localization__group_subtypes__label" comment="Label of subtypes group">Sotipos</string>
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">Amestar un sotipu</string>
<string name="settings__localization__language_pack_title" comment="Title of the language pack manager screen for managing installed and custom language packs">Xestionar los paquetes de llingua instalaos</string>
<string name="settings__localization__language_pack_summary" comment="Summary of preference item for adding a new language pack">Opción esperimental: xestiona les estensiones qu\'amiesten compatibilidá pa llingües específiques (pel momentu, entrada china basada en formes)</string>
<string name="settings__localization__subtype_locale" comment="Label for locale dropdown in subtype dialog">Llingua primaria</string>
@@ -65,6 +72,12 @@
<string name="settings__localization__subtype_phone_layout" comment="Label for layout dropdown in subtype dialog">Distribución primaria del tecláu telefónicu</string>
<string name="settings__localization__subtype_phone2_layout" comment="Label for layout dropdown in subtype dialog">Distribución secundaria del tecláu telefónicu</string>
<string name="settings__localization__subtype_summary" comment="Subtype summary">{characters_name} / {symbols_name} / {currency_set_name}</string>
<string name="settings__localization__suggested_subtype_presets" comment="Suggested presets title">Sotipos preconfiguraos suxeríos</string>
<string name="settings__localization__suggested_subtype_presets_none_found" comment="Suggested presets none found">Nun hai nengún stipu suxeríu. Usa\'l botón d\'abaxo pa ver tolos sotipos preconfiguraos.</string>
<string name="settings__localization__subtype_presets" comment="Subtype presets dialog title">Sotipos preconfiguraos</string>
<string name="settings__localization__subtype_presets_view_all" comment="View all presets button">Amosar too</string>
<string name="settings__localization__subtype_no_subtypes_configured_warning" comment="Warning message that no subtype has been defined">Paez que nun configuresti nengún sotipu. ¡Va usase\'l sotipu «English/QWERTY» como alternativa!</string>
<string name="settings__localization__subtype_error_already_exists" comment="Error message shown in subtype dialog when a subtype to add already exists">¡Esti sotipu yá esiste!</string>
<string name="settings__localization__group_layouts__label" comment="Label of layouts group">Distribuciones</string>
<string name="settings__theme__title" comment="Title of the Theme screen">Estilu</string>
<string name="pref__theme__mode__label" comment="Label of the theme mode preference">Mou del estilu</string>
@@ -72,10 +85,9 @@
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Hora del aséu</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Estilu diurnu</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Estilu nocherniegu</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Estilu seleicionáu</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Xestionar los estilos instalaos</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Almacenamientu internu</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">Fornidor esternu</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Xestionar los estilos instalaos</string>
<string name="settings__theme_editor__no_rules_defined">Esta fueya d\'estilu nun tien nenguna regla definida. Amiesta una regla pa comenzar a personalizar la fueya d\'estilu.</string>
<string name="settings__theme_editor__rule_element">Elementu de destín</string>
<string name="settings__theme_editor__rule_groups">Grupos</string>
@@ -145,6 +157,7 @@
<string name="pref__gestures__space_bar_title" comment="Preference group title">Xestos de la barra d\'espaciu</string>
<string name="settings__advanced__title" comment="Title of Advanced settings">Configuración avanzada</string>
<string name="pref__advanced__settings_theme__label" comment="Label of Settings theme preference in Advanced">Estilu de la configuración</string>
<string name="pref__advanced__settings_material_you__label" comment="Label of Material You preference in Advanced">Usar «Material You»</string>
<string name="pref__advanced__settings_language__label" comment="Label of Settings language preference in Advanced">Llingua de la configuración</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">Amosar l\'iconu l\'aplicación nel llanzador</string>
<!-- About UI strings -->
@@ -156,7 +169,7 @@
<string name="about__version_copied__title" comment="Title of the toast for copying the version string">La versión copióse al cartafueyu</string>
<string name="about__version_copied__error" comment="Title of the error toast for copying the version string">Prodúxose daqué malo: {error_message}</string>
<string name="about__changelog__title" comment="Preference title">Rexistru de cambeos</string>
<string name="about__changelog__summary" comment="Preference summary">Les novedaes</string>
<string name="about__changelog__summary" comment="Preference summary">Contién les novedaes</string>
<string name="about__repository__title" comment="Preference title">Depósitu (GitHub)</string>
<string name="about__repository__summary" comment="Preference summary">El códigu fonte, los discutinios, los problemes y más información</string>
<string name="about__privacy_policy__title" comment="Preference title">Política de privacidá</string>
@@ -167,15 +180,25 @@
<string name="about__third_party_licenses__summary" comment="Preference summary">Les llicencies de les biblioteques de terceros que s\'inclúin nesta aplicación</string>
<!-- Setup UI strings -->
<string name="setup__title" comment="Title of Setup">¡Afáyate!</string>
<string name="setup__intro_message" comment="Short intro message welcoming new users">¡Gracies por usar {app_name}! Esta configuración rápida va guiate pelos pasos necesarios pa usar {app_name} nel preséu.</string>
<string name="setup__footer__privacy_policy" comment="Privacy policy label for URL">Política de privacidá</string>
<string name="setup__footer__repository" comment="Repository label for URL">Depósitu</string>
<string name="setup__enable_ime__title">Activación de {app_name}</string>
<string name="setup__enable_ime__description">Android rique que tolos teclaos personalizaos s\'activen per separtao enantes de poder usalos. Vete a la configuración de <i>Llingua y entrada</i> p\'activar «{app_name}» ellí.</string>
<string name="setup__enable_ime__open_settings_btn">Abrir la configuración del sistema</string>
<string name="setup__select_ime__title">Seleición de {app_name}</string>
<string name="setup__select_ime__description">{app_name} yá ta activáu nel sistema. Pa usalu, escueyi «{app_name}» nel diálogu del selector d\'entrada.</string>
<string name="setup__select_ime__switch_keyboard_btn">Cambiar de tecláu</string>
<string name="setup__grant_notification_permission__title">Avisos pa informar de casques</string>
<string name="setup__grant_notification_permission__description">A partir d\'Android 13, les aplicaciones tienen de pidir
permisu pa unviar avisos. En FlorisBoard, esti permisu namás s\'usa p\'abrir la pantalla d\'informar casques al producise dalgún.
Pues camudar esti permisu cuando quieras na configuración del sistema.
</string>
<string name="setup__grant_notification_permission__btn">Conceder el permisu</string>
<string name="setup__finish_up__title">Fin</string>
<string name="setup__finish_up__description_p1">{app_name} ta activáu nel sistema y yá pues personalizalu.</string>
<string name="setup__finish_up__description_p2">Si atopes cualesquier problema, fallu, casque o namás quier facer dalguna suxerencia, revisa\'l depósitu del proyeutu na pantalla «Tocante a».</string>
<string name="setup__finish_up__description_p2">Si atopes cualesquier problema, fallu, casque o namás quier facer dalguna suxerencia, consulta\'l depósitu del proyeutu na pantalla «Tocante a».</string>
<string name="setup__finish_up__finish_btn">Comenzar a personalizar</string>
<!-- Back up & Restore -->
<string name="backup_and_restore__back_up__destination_file_sys">Sistema de ficheros llocal</string>
<string name="backup_and_restore__back_up__destination_share_intent">Aplicación de terceros pel menú de compartición</string>
@@ -205,7 +228,7 @@
<string name="send_to_clipboard__type_not_supported_error">Esti elementu multimedia nun ye compatible.</string>
<!-- Devtools strings -->
<string name="devtools__title" comment="Title of Devtools screen. Translators: treat this string as 'Developer tools' for translation, except a similar short term is available for your language.">Ferramientes de desendolcu</string>
<string name="devtools__enabled__summary" comment="Summary of Enable developer tools in Devtools">Ferramientes diseñaes específicamente pa depurar y iguar problemes</string>
<string name="devtools__enabled__summary" comment="Summary of Enable developer tools in Devtools">Ferramientes diseñaes específicamente pa depurar ya iguar problemes</string>
<string name="devtools__show_key_touch_boundaries__label" comment="Label of Show key touch boundaries in Devtools">Amosar les llendes táctiles de les tecles</string>
<string name="devtools__show_drag_and_drop_helpers__label" comment="Label of Show drag and drop helpers in Devtools">Amosar los ayudantes de la función «drag&amp;drop»</string>
<string name="devtools__reset_flag_is_ime_set_up__summary" comment="Summary of Reset is IME set up flag in Devtools">Una aición de depuración pa volver amosar la pantalla de configuración</string>
@@ -217,6 +240,7 @@
<string name="devtools__android_settings_system__title" comment="Title of Android settings (system) screen">Configuración del sistema Android</string>
<string name="devtools__android_locales__title" comment="Title of Android locales screen">Locales del sistema</string>
<!-- Extension strings -->
<string name="ext__home__title">Complementos y estensiones</string>
<string name="ext__meta__description">Descripción</string>
<string name="ext__meta__id">ID</string>
<string name="ext__meta__keywords">Pallabres clave</string>
@@ -226,13 +250,20 @@
<string name="ext__error__not_found_description">Nun se pudo atopar nenguna estensión cola ID «{id}».</string>
<string name="ext__editor__title_create_any">Creación d\'una estensión</string>
<string name="ext__editor__metadata__title_invalid">Los metadatos nun son válidos</string>
<string name="ext__editor__metadata__message_invalid">Los metadatos d\'esta estensión nun son válidos. ¡Revisa l\'editor de metadatos pa consiguir más detalles!</string>
<string name="ext__editor__metadata__message_invalid">Los metadatos d\'esta estensión nun son válidos. ¡Consulta l\'editor de metadatos pa consiguir más detalles!</string>
<string name="ext__editor__dependencies__title">Xestionar les dependencies</string>
<string name="ext__editor__create_component__title_theme">Creación d\'un estilu</string>
<string name="ext__export__failure">La esportación de la estensión falló: {error_message}</string>
<string name="ext__import__failure">La importación de la estensión falló: {error_message}</string>
<string name="ext__import__file_skip" comment="Label when a file cannot be imported in the current context. The actual reason string is in a separate text view below this string.">El ficheru nun se pue importar. Motivu:</string>
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">Prodúxose un error inesperáu demientres la importación. Forniéronse los detalles siguientes:</string>
<string name="ext__validation__enter_version">Introduz una versión</string>
<string name="ext__validation__enter_title">Introduz un títulu</string>
<string name="ext__validation__enter_valid_number">Introduz nun númberu válidu</string>
<string name="ext__validation__enter_positive_number">Introduz un númberu positivu (&gt;=0)</string>
<string name="ext__update_box__search_for_updates">Buscar anovamientos</string>
<string name="ext__addon_management_box__go_to_page">Dir a «{ext_home_title}»</string>
<string name="ext__home__visit_store">Visitar la tienda de complementos</string>
<!-- Action strings -->
<string name="action__add">Amestar</string>
<string name="action__apply">Aplicar</string>

View File

@@ -13,9 +13,24 @@
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">Емоции</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">Емоции</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">Каомоджи</string>
<string name="prefs__media__emoji_recently_used_max_size">Дължина на историята на емоциите</string>
<string name="prefs__media__emoji_preferred_skin_tone">Предпочитан цвят на кожата за емоции</string>
<string name="prefs__media__emoji_preferred_hair_style">Предпочитана прическа за емоции</string>
<string name="prefs__media__emoji_history__title" comment="Preference group title">История на емоциите</string>
<string name="prefs__media__emoji_history_enabled" comment="Preference title">Включване на историята на емоциите</string>
<string name="prefs__media__emoji_history_enabled__summary" comment="Preference summary">Запазване на използваните емоции за по-бърз достъп</string>
<string name="prefs__media__emoji_history_pinned_update_strategy" comment="Preference title">Стратегия за обновяване (закачени)</string>
<string name="prefs__media__emoji_history_recent_update_strategy" comment="Preference title">Стратегия за обновяване (последни)</string>
<string name="prefs__media__emoji_history_max_size">Максимален брой елементи</string>
<string name="prefs__media__emoji_suggestion__title" comment="Preference group title">Предложения за емоции</string>
<string name="prefs__media__emoji_suggestion_enabled" comment="Preference title">Включване на предложенията за емоции</string>
<string name="prefs__media__emoji_suggestion_enabled__summary" comment="Preference summary">Показва предложения за емоции докато въвеждате</string>
<string name="prefs__media__emoji_suggestion_type" comment="Preference title">Вид на задействането</string>
<string name="prefs__media__emoji_suggestion_update_history" comment="Preference title">Обновяване на историята на емоциите</string>
<string name="prefs__media__emoji_suggestion_update_history__summary" comment="Preference summary">Приемане на предложената емиция я добавя в историята на емоции</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name" comment="Preference title">Показване на името на емоцията</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name__summary" comment="Preference summary">Предложенията за емоции включват и името</string>
<string name="prefs__media__emoji_suggestion_query_min_length" comment="Preference title">Минимална дължина на заявка</string>
<string name="prefs__media__emoji_suggestion_candidate_max_count" comment="Preference title">Максимален брой кандидати</string>
<!-- Emoji strings -->
<string name="emoji__category__smileys_emotion" comment="Emoji category name">Усмивки и емоции</string>
<string name="emoji__category__people_body" comment="Emoji category name">Хора и тяло</string>
@@ -26,10 +41,12 @@
<string name="emoji__category__objects" comment="Emoji category name">Предмети</string>
<string name="emoji__category__symbols" comment="Emoji category name">Символи</string>
<string name="emoji__category__flags" comment="Emoji category name">Знамена</string>
<string name="emoji__recently_used__empty_message" comment="Message if no recently used emojis exist">Скоро не сте ползвали еможи. Като започнете да използвате ще се показват тук.</string>
<string name="emoji__recently_used__phone_locked_message" comment="Message to show if phone is locked">За достъп до последно използваните емоции отключете устройството.</string>
<string name="emoji__recently_used__removal_tip" comment="Feature discoverability for removal of recently used emojis">Подсказка: За да премахнете емоциите от този списък задръжте върху него!</string>
<string name="emoji__recently_used__removal_success_message" comment="Toast message if user has long pressed emoji in recently used collection to remove it">Премахнато е {emoji} от последно използваните</string>
<string name="emoji__history__empty_message" comment="Message if the emoji history is empty">Скоро не сте използвали емоции. Започнете да използвате и ще бъдат показани тук.</string>
<string name="emoji__history__phone_locked_message" comment="Message to show if phone is locked">За достъп до последно използваните емоции отключете устройството.</string>
<string name="emoji__history__usage_tip" comment="Feature discoverability for actions of emoji history">Подсказка: Задръжте върху емоция в историята, за да я закачите или премахнате!</string>
<string name="emoji__history__removal_success_message" comment="Toast message if user has used the delete action on an emoji in the emoji history">Емоцията {emoji} е премахната от историята</string>
<string name="emoji__history__pinned">Закачени</string>
<string name="emoji__history__recent">Последни</string>
<!-- Quick action strings -->
<string name="quick_action__arrow_up" maxLength="12">Нагоре</string>
<string name="quick_action__arrow_up__tooltip">Извършва стрелка нагоре</string>
@@ -89,13 +106,13 @@
<string name="settings__system_default" comment="General string which is used when a preference has the system default value set">По подразбиране от системата</string>
<string name="settings__home__title" comment="Title of the Home screen">Добре дошли във {app_name}</string>
<string name="settings__home__ime_not_enabled" comment="Error message shown in Home fragment when FlorisBoard is not enabled in the system">Клавиатурата FlorisBoard не е активна в системата и не е достъпна като метод за въвеждане. Докоснете тук, за да промените.</string>
<string name="settings__home__ime_not_selected" comment="Warning message shown in Home fragment when FlorisBoard is not selected as the default keyboard">Клавиатурата FlorisBoard не е избрана като подразбиран метод за въвеждане. Докоснете тук, за да направите промяна.</string>
<string name="settings__home__ime_not_selected" comment="Warning message shown in Home fragment when FlorisBoard is not selected as the default keyboard">Клавиатурата FlorisBoard не е избрана като подразбиран метод за въвеждане. Докоснете, за да направите промяната.</string>
<string name="settings__localization__title" comment="Title of languages and Layout screen">Езици и подредби</string>
<string name="settings__localization__display_language_names_in__label" comment="Label of Display language names in preference">Изписване на езиците на</string>
<string name="settings__localization__group_subtypes__label" comment="Label of subtypes group">Подредби</string>
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">Добавяне на подредба</string>
<string name="settings__localization__language_pack_title" comment="Title of the language pack manager screen for managing installed and custom language packs">Управление на инсталираните езикови пакети</string>
<string name="settings__localization__language_pack_summary" comment="Summary of preference item for adding a new language pack">Ексоериментално: управление на добавки за поддръжка на ооределени езици (въвеждане чрез фигури на китайски език)</string>
<string name="settings__localization__language_pack_summary" comment="Summary of preference item for adding a new language pack">Експериментално: управление на добавки за поддръжка на определени езици (въвеждане чрез фигури на китайски език)</string>
<string name="settings__localization__subtype_edit_title" comment="Title of subtype dialog when editing an existing subtype">Променяне на подредба</string>
<string name="settings__localization__subtype_locale" comment="Label for locale dropdown in subtype dialog">Основен език</string>
<string name="settings__localization__subtype_popup_mapping" comment="Label for popup mapping dropdown in subtype screen">Подсказки</string>
@@ -124,21 +141,20 @@
<string name="settings__localization__subtype_error_fields_no_value" comment="Error message shown in subtype editor if at least one field is set to '- select -' (means no value specified)">Има поле е без стойност. Изберете стойност за всички полета.</string>
<string name="settings__localization__subtype_error_layout_not_installed" comment="Error message shown in subtype list when a layout is not installed, where %s will be replaced by the layout ID">{layout_id} (не е инсталирано)</string>
<string name="settings__localization__group_layouts__label" comment="Label of layouts group">Подредби</string>
<string name="settings__localization__subtype_delete_confirmation_title" comment="Title of the subtype delete confirmation dialog">Потвърждение при премахване</string>
<string name="settings__localization__subtype_delete_confirmation_warning" comment="Warning message in the confirmation dialog to confirm the user's intent to delete">Желаете ли да премахнете този подвид?</string>
<string name="settings__theme__title" comment="Title of the Theme screen">Тема</string>
<string name="pref__theme__mode__label" comment="Label of the theme mode preference">Режим на тема</string>
<string name="pref__theme__sunrise_time__label" comment="Label of the sunrise time preference">Време на изгрев</string>
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Време на залез</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Дневна тема</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Нощна тема</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Избрана тема</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Адаптиращи се цветове</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Темата се адаптира спрямо текущото приложение, ако то го поддържа.</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Управление на теми</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">Активи на FlorisBoard</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Вътрешно хранилище</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">Външен доставчик</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Избор на дневна тема</string>
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Избор на нощна тема</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Управление на теми</string>
<string name="settings__theme_editor__fine_tune__title">Настройки на редактора</string>
<string name="settings__theme_editor__fine_tune__level">Режим</string>
<string name="settings__theme_editor__fine_tune__display_colors_as">Формат на цветовете</string>
@@ -311,6 +327,7 @@
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">Закъснение при задържане на клавиш</string>
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">Клавишът интервал превключва към букви</string>
<string name="pref__keyboard__space_bar_switches_to_characters__summary" comment="Preference summary">Автоматично превключва към букви ако се въвеждат символи или цифри</string>
<string name="pref__keyboard__incognito_indicator__label" comment="Preference title">Индикатор за инкогнито</string>
<!-- Smartbar strings -->
<string name="settings__smartbar__title" comment="Title of Smartbar screen">Интелигентна лента</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">Интелигентна лента</string>
@@ -413,6 +430,7 @@
<string name="pref__advanced__settings_theme__light" comment="Possible value of Settings theme preference in Advanced">Светла</string>
<string name="pref__advanced__settings_theme__dark" comment="Possible value of Settings theme preference in Advanced">Тъмна</string>
<string name="pref__advanced__settings_theme__amoled_dark" comment="Possible value of Settings theme preference in Advanced">Тъмна AMOLED</string>
<string name="pref__advanced__settings_material_you__label" comment="Label of Material You preference in Advanced">Използване на Material You</string>
<string name="pref__advanced__settings_language__label" comment="Label of Settings language preference in Advanced">Език на настройките</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">Икона на приложението в стартовия панел</string>
<string name="pref__advanced__show_app_icon__summary_atleast_q" comment="Summary of Show app icon preference in Advanced for Android 10+">Винаги видима за Android 10+ поради ограничения на системата</string>
@@ -470,6 +488,10 @@
<string name="backup_and_restore__back_up__files_ime_keyboard">Добавки за клавиатури</string>
<string name="backup_and_restore__back_up__files_ime_spelling">Добавки за правопис / речници</string>
<string name="backup_and_restore__back_up__files_ime_theme">Добавки за теми</string>
<string name="backup_and_restore__back_up__files_clipboard_history">История на междинната памет</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_text_items">Текстови елементи</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_image_items">Изображения</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_video_items">Видеоклипове</string>
<string name="backup_and_restore__back_up__success">Резервното копие е изнесено успешно!</string>
<string name="backup_and_restore__back_up__failure">Грешка при изнасяне на резервно копие: {error_message}</string>
<string name="backup_and_restore__restore__title">Възстановяване на данни</string>
@@ -510,7 +532,7 @@
<string name="clipboard__locked__message">За достъп до междинната памет отключете устройството.</string>
<string name="clipboard__group_pinned">Закачени</string>
<string name="clipboard__group_recent">Последни</string>
<string name="clipboard__group_other">Друго</string>
<string name="clipboard__group_other">Други</string>
<string name="clipboard__item_description_email">Електронна поща</string>
<string name="clipboard__item_description_url">Адрес</string>
<string name="clipboard__item_description_phone">Телефон</string>
@@ -537,7 +559,7 @@
<string name="pref__clipboard__enable_clipboard_history__summary">Запазване на копираното за по-бърз достъп до него</string>
<string name="pref__clipboard__clean_up_old__label">Премахване на стари елементи</string>
<string name="pref__clipboard__clean_up_after__label">Премахване на старите елементи след</string>
<string name="pref__clipboard__limit_history_size__label">Ограничаване на броя</string>
<string name="pref__clipboard__limit_history_size__label">Ограничаване на броя елементи</string>
<string name="pref__clipboard__max_history_size__label">Максимален брой елементи</string>
<string name="pref__clipboard__clear_primary_clip_deletes_last_item__label">Изчистване на текущата межд. памет влияе на историята</string>
<string name="pref__clipboard__clear_primary_clip_deletes_last_item__summary">Изчистване на текущото съдържание на междинната памет изчиства и последния елемент от историята</string>
@@ -557,6 +579,8 @@
<string name="devtools__show_input_state_overlay__summary" comment="Summary of Show input cache overlay in Devtools">Слой със състоянието на полето за въвеждане, за отстраняване на дефекти</string>
<string name="devtools__show_spelling_overlay__label" comment="Label of Show spelling overlay in Devtools">Слой за проверка на правописа</string>
<string name="devtools__show_spelling_overlay__summary" comment="Summary of Show spelling overlay in Devtools">Слой със съдържанието от проверката на правопис, за отстраняване на дефекти</string>
<string name="devtools__show_inline_autofill_overlay__label">Автоматично попълване в слой</string>
<string name="devtools__show_inline_autofill_overlay__summary">Слой с резултатите от автоматичното попълване, за отстраняване на дефекти</string>
<string name="devtools__show_key_touch_boundaries__label" comment="Label of Show key touch boundaries in Devtools">Видим контур на клавишите</string>
<string name="devtools__show_key_touch_boundaries__summary" comment="Summary of Show key touch boundaries in Devtools">Оцветяване на контура на клавишите в червено</string>
<string name="devtools__show_drag_and_drop_helpers__label" comment="Label of Show drag and drop helpers in Devtools">Манипулатори за влачене и пускане</string>
@@ -572,7 +596,16 @@
<string name="devtools__android_settings_secure__title" comment="Title of Android settings (secure) screen">Настройки на сигурността на Android</string>
<string name="devtools__android_settings_system__title" comment="Title of Android settings (system) screen">Системни настройки на Android</string>
<string name="devtools__android_locales__title" comment="Title of Android locales screen">Езици на системата</string>
<string name="devtools__debuglog__title">Дневник за отстраняване на дефекти</string>
<string name="devtools__debuglog__copied_to_clipboard">Съдържанието на дневника е копирано</string>
<string name="devtools__debuglog__copy_log">Копиране</string>
<string name="devtools__debuglog__copy_for_github">Копиране (форматирано за Гитхъб)</string>
<string name="devtools__debuglog__loading">Зареждане…</string>
<!-- Extension strings -->
<string name="ext__home__title">Добавки и разширения</string>
<string name="ext__list__ext_theme">Добавки за теми</string>
<string name="ext__list__ext_keyboard">Добавки за клавиатури</string>
<string name="ext__list__ext_languagepack">Езикови пакети</string>
<string name="ext__meta__authors">Автори</string>
<string name="ext__meta__components">Придружаващи компоненти</string>
<string name="ext__meta__components_theme">Придружаващи теми</string>
@@ -625,13 +658,47 @@
<string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">Очакваше се медиен файл (изображение, аудио, шрифт, т.н.), но бе намерен архив с разширение.</string>
<string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">Очакваше се архив с разширение, но бе намерен медиен файл (изображение, аудио, шрифт, т.н.).</string>
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">Неочаквана грешка при внасяне. Предоставени са следните подробности:</string>
<string name="ext__validation__enter_package_name">Въведете име на пакета</string>
<string name="ext__validation__error_package_name">Името на пакетът не удовлетворява регулярния израз {id_regex}</string>
<string name="ext__validation__enter_version">Въведете издание</string>
<string name="ext__validation__enter_title">Въведете заглавие</string>
<string name="ext__validation__enter_maintainer">Въведете най-малко един отговорник</string>
<string name="ext__validation__enter_license">Въведете идентификатор на лиценз</string>
<string name="ext__validation__enter_component_id">Въведете идентификатор на компонент</string>
<string name="ext__validation__error_component_id">Въведете идентификатор на компонент, удовлетворяващ регулярния израз {component_id_regex}</string>
<string name="ext__validation__enter_component_label">Въведете етикет на компонент</string>
<string name="ext__validation__hint_component_label_to_long">Етикетът на компонента е твърде дълъг, което може да доведе до отрязване в интерфейса</string>
<string name="ext__validation__error_author">Въведете най-малко един автор</string>
<string name="ext__validation__error_stylesheet_path_blank">Пътят към стиловия лист не трябва да е празен</string>
<string name="ext__validation__error_stylesheet_path">Въведете път към стилов лист, удовлетворяващ регулярния израз {stylesheet_path_regex}</string>
<string name="ext__validation__enter_property">Въведете име на променливата</string>
<string name="ext__validation__error_property">Въведете име на променлива, удовлетворяващо регулярния израз {variable_name_regex}</string>
<string name="ext__validation__hint_property" tools:ignore="TypographyDashes">По правило имената на променливите на FlorisCSS започват с две тирета (--)</string>
<string name="ext__validation__enter_color">Въведете низ на цвят</string>
<string name="ext__validation__error_color">Въведете низ на съществуващ цвят</string>
<string name="ext__validation__enter_dp_size">Въведете размер на dp</string>
<string name="ext__validation__enter_valid_number">Въведете число</string>
<string name="ext__validation__enter_positive_number">Въведете положително число (&gt;=0)</string>
<string name="ext__validation__enter_percent_size">Въведете размер в проценти</string>
<string name="ext__validation__enter_number_between_0_100">Въведете положително число между 0 и 100</string>
<string name="ext__validation__hint_value_above_50_percent">Стойности над 50% ще бъдат приравнени на 50%, така че намалете стойността</string>
<string name="ext__update_box__internet_permission_hint">Понеже приложението няма достъп до интернет, проверката за обновяване на инсталираните разширения се извършва ръчно.</string>
<string name="ext__update_box__search_for_updates">Проверка за обновяване</string>
<string name="ext__addon_management_box__managing_placeholder">Управление на {extensions}</string>
<string name="ext__addon_management_box__addon_manager_info">Всички дейности, свързани с внасяне, изнасяне, създаване, промяна и премахване на разширения могат да бъдат извършване през Управление на разширения.</string>
<string name="ext__addon_management_box__go_to_page">Към {ext_home_title}</string>
<string name="ext__home__info">Можете да изтегляте и инсталирате разширения от магазина за добавки на FlorisBoard или да внесете файл на разширение, който сте изтеглили от интернет.</string>
<string name="ext__home__visit_store">Магазин за добавки</string>
<string name="ext__home__manage_extensions">Управление на инсталирани разширения</string>
<string name="ext__list__view_details">Подробности</string>
<string name="ext__check_updates__title">Проверка за обновяване</string>
<!-- Action strings -->
<string name="action__add">Добавяне</string>
<string name="action__apply">Прилагане</string>
<string name="action__back_up">Резервно копие</string>
<string name="action__cancel">Отказ</string>
<string name="action__create">Създаване</string>
<string name="action__default">По подразбиране</string>
<string name="action__default">Стандартни</string>
<string name="action__delete">Премахване</string>
<string name="action__delete_confirm_title">Потвърждаване на изтриване</string>
<string name="action__delete_confirm_message">Наистина ли искате да изтриете „{name}“? Това действие веднъж изпълнено не може да бъде отменено.</string>
@@ -692,6 +759,14 @@
<string name="enum__display_language_names_in__system_locale__description" comment="Enum value description">Имената на езиците в приложението и клавиатурата са на подразбирания за устройствто език</string>
<string name="enum__display_language_names_in__native_locale" comment="Enum value label">Присъщия за езика</string>
<string name="enum__display_language_names_in__native_locale__description" comment="Enum value description">Имената на езиците в приложението и клавиатурата са на присъщия за езика</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend" comment="Enum value label">Автом. сортиране (добавяне отпред)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend__description" comment="Enum value description">Автоматично сортиране при използване на емоция. Новите емоции биват добавени в началото.</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append" comment="Enum value label">Автом. сортиране (добавяне отзад)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append__description" comment="Enum value description">Автоматично сортиране при използване на емоция. Новите емоции биват добавени в края.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend" comment="Enum value label">Ръчно сортиране (добавяне отпред)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend__description" comment="Enum value description">Без автоматично сортиране при използване на емоция. Новите емоции биват добавени в началото.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append" comment="Enum value label">Ръчно сортиране (добавяне отзад)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append__description" comment="Enum value description">Без автоматично сортиране при използване на емоция. Новите емоции биват добавени в края.</string>
<string name="enum__emoji_skin_tone__default" comment="Enum value label">{emoji} Подразбиран цвят на кожата</string>
<string name="enum__emoji_skin_tone__light_skin_tone" comment="Enum value label">{emoji} Светъл цвят на кожа</string>
<string name="enum__emoji_skin_tone__medium_light_skin_tone" comment="Enum value label">{emoji} Средно светъл цвят на кожата</string>
@@ -703,6 +778,10 @@
<string name="enum__emoji_hair_style__curly_hair" comment="Enum value label">{emoji} Къдрава коса</string>
<string name="enum__emoji_hair_style__white_hair" comment="Enum value label">{emoji} Бял цвят на косата</string>
<string name="enum__emoji_hair_style__bald" comment="Enum value label">{emoji} Без коса</string>
<string name="enum__emoji_suggestion_type__leading_colon">Водещо двоеточие</string>
<string name="enum__emoji_suggestion_type__leading_colon__description" comment="Keep the :emoji_name while translating, this is a syntax guide">Предлагане на емоции чрез синтаксиса :имеа_емоция</string>
<string name="enum__emoji_suggestion_type__inline_text">Обикновен текст</string>
<string name="enum__emoji_suggestion_type__inline_text__description">Предлагане на емоции чрез въвеждане на името на емоцията като дума</string>
<string name="enum__extended_actions_placement__above_candidates" comment="Enum value label">Над кандидатите</string>
<string name="enum__extended_actions_placement__above_candidates__description" comment="Enum value description">Добавя се реда с допълнителни действия между приложението и реда с кандидати</string>
<string name="enum__extended_actions_placement__below_candidates" comment="Enum value label">Под кандидатите</string>
@@ -719,6 +798,8 @@
<string name="enum__key_hint_mode__hint_priority__description" comment="Enum value description">Първоначално избраният знак след задържане на клавиш винаги е знака от подсказката или основния акцент ако в подсказката няма знак</string>
<string name="enum__key_hint_mode__smart_priority" comment="Enum value label">Интелигентен приоритет</string>
<string name="enum__key_hint_mode__smart_priority__description" comment="Enum value description">Първоначално избраният знак при задържане на клавиш се определя между основния акцент или знака от подсказката на базата на текущия език и подредба</string>
<string name="enum__incognito_display_mode__replace_shared_actions_toggle" comment="Enum value label">Заменя пиктограмата на клавиша за превкл. на действията с индикатора за инкогнито</string>
<string name="enum__incognito_display_mode__display_behind_keyboard" comment="Enum value label">Показва индикатора за инкогнито зад клавиатурата</string>
<string name="enum__incognito_mode__force_off" comment="Enum value label">Принудително изключено</string>
<string name="enum__incognito_mode__force_off__description" comment="Enum value description">Режим „инкогнито“ винаги ще бъде изключен, независимо какво подава целевото приложение. Клавишът за действието няма да бъде достъпен в интелигентната лента.</string>
<string name="enum__incognito_mode__force_on" comment="Enum value label">Принудително включено</string>
@@ -816,4 +897,12 @@
<item quantity="one">{v} елемент</item>
<item quantity="other">{v} елемента</item>
</plurals>
<plurals name="unit__characters__written">
<item quantity="one">{v} знак</item>
<item quantity="other">{v} знака</item>
</plurals>
<plurals name="unit__candidates__written">
<item quantity="one">{v} кандидат</item>
<item quantity="other">{v} кандидата</item>
</plurals>
</resources>

View File

@@ -35,8 +35,6 @@
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Vrijeme zalaska sunca</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Dnevna tema</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Noćna tema</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Odabrana tema</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Adaptiraj boje prema aplikaciji</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Interna memorija</string>
<string name="settings__keyboard__title" comment="Title of Keyboard preferences screen">Tastatura</string>
<string name="pref__keyboard__number_row__label" comment="Preference title">Red sa brojevima</string>

View File

@@ -13,7 +13,6 @@
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">Emojis</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">Emoticones</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">Kaomoji</string>
<string name="prefs__media__emoji_recently_used_max_size">Mida màxima de l\'historial d\'emojis</string>
<string name="prefs__media__emoji_preferred_skin_tone">To de pell preferit dels emojis</string>
<string name="prefs__media__emoji_preferred_hair_style">Pentinat preferit dels emojis</string>
<!-- Emoji strings -->
@@ -26,9 +25,6 @@
<string name="emoji__category__objects" comment="Emoji category name">Objectes</string>
<string name="emoji__category__symbols" comment="Emoji category name">Símbols</string>
<string name="emoji__category__flags" comment="Emoji category name">Banderes</string>
<string name="emoji__recently_used__empty_message" comment="Message if no recently used emojis exist">No s\'han trobat emojis recents. En començar a escriure emojis, apareixeran automàticament aquí.</string>
<string name="emoji__recently_used__removal_tip" comment="Feature discoverability for removal of recently used emojis">Consell: Manteniu premut els emojis recents per a suprimir-los d\'aquesta llista!</string>
<string name="emoji__recently_used__removal_success_message" comment="Toast message if user has long pressed emoji in recently used collection to remove it">S\'ha suprimit {emoji} dels emojis recents</string>
<!-- Quick action strings -->
<!-- Incognito mode strings -->
<!-- Settings UI strings -->
@@ -80,15 +76,12 @@
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Hora de posta de sol</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Tema diürn</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Tema nocturn</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Selecció del tema</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Adapta els colors a l\'aplicació</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Els colors del tema s\'adapten als de l\'aplicació actual, si l\'aplicació de destinació ho admet.</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Gestiona els temes instal·lats</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">Actius d\'aplicacions FlorisBoard</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Emmagatzematge intern</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">Proveïdor extern</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Selecciona el tema del dia</string>
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Selecciona el tema de nit</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Gestiona els temes instal·lats</string>
<string name="settings__theme_editor__fine_tune__display_colors_as">Mostrar els colors com</string>
<string name="settings__theme_editor__add_rule">Afegir norma</string>
<string name="settings__theme_editor__edit_rule">Editar norma</string>

View File

@@ -13,7 +13,6 @@
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">خەندەکان</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">هێمای دەربڕینی هەستەکان</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">کامۆجی</string>
<string name="prefs__media__emoji_recently_used_max_size">ڕێژەی پیشاندانی خەندە لە مێژووی خەندەکان</string>
<string name="prefs__media__emoji_preferred_skin_tone">ڕەنگی بنەڕەتی خەندەکان</string>
<string name="prefs__media__emoji_preferred_hair_style">ڕەنگی بنەڕەتی قژی خەندەکان</string>
<!-- Emoji strings -->
@@ -26,9 +25,6 @@
<string name="emoji__category__objects" comment="Emoji category name">ئامانجەکان</string>
<string name="emoji__category__symbols" comment="Emoji category name">هێماکان</string>
<string name="emoji__category__flags" comment="Emoji category name">ئاڵاکان</string>
<string name="emoji__recently_used__empty_message" comment="Message if no recently used emojis exist">هیچ ئیمۆجیەک بەکارنەهاتووە، هەرکاتێ ئیمۆجیەکانت بەکارهێنا بەشێوەیەکی خۆکاری لێرەدا دەردەکەون.</string>
<string name="emoji__recently_used__removal_tip" comment="Feature discoverability for removal of recently used emojis">زانیاری: بۆ سڕینەوەی ئیمۆجیەکان لەم بەشەدا کەمێک دەستی لەسەر ڕابگرە.</string>
<string name="emoji__recently_used__removal_success_message" comment="Toast message if user has long pressed emoji in recently used collection to remove it">ئیمۆجی {emoji} لابرا لەلیستی دوایین بەکارهاتووەکان</string>
<!-- Quick action strings -->
<string name="quick_action__arrow_up" maxLength="12">چونە سەرەوە</string>
<string name="quick_action__arrow_up__tooltip">چوونە سەرەتای دێڕ</string>
@@ -129,15 +125,12 @@
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">کاتی خۆر ئاوابوون</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">ڕووناک</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">تاریک</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">ڕووکاری هەڵبژێردراو</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">گۆڕینی ڕەنگی ڕووکار بەپێی بەرنامەکان</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">ڕەنگی ڕووکار بەخۆکاری دەگۆڕێت لەگەڵ ڕەنگی بەرنامەکان ئەگەر بەرنامەکە پشتگیری هەمەڕەنگ بێت.</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">ڕێکخستنی رووکارەکان</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">ڕەنگی بنەڕەتی بەرنامەکە</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">بیرگەی ناخۆیی</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">بیرگەی دەرەکی</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">ڕووکارەکانی دۆخی ڕووناک</string>
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">ڕووکارەکانی دۆخی تاریک</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">ڕێکخستنی رووکارەکان</string>
<string name="settings__theme_editor__fine_tune__title">گۆڕینی ڕەنگ</string>
<string name="settings__theme_editor__fine_tune__level">ئاستی دەستکاریکردن</string>
<string name="settings__theme_editor__fine_tune__display_colors_as">پیشاندانی ڕەنگەکان بەشێوەی</string>

View File

@@ -13,9 +13,24 @@
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">Emotikony</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">Emotikony</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">Kaomoji</string>
<string name="prefs__media__emoji_recently_used_max_size">Maximální velikost historie emotikonů</string>
<string name="prefs__media__emoji_preferred_skin_tone">Preferovaná barva pokožky emotikonů</string>
<string name="prefs__media__emoji_preferred_hair_style">Preferovaný styl vlasů emotikonů</string>
<string name="prefs__media__emoji_history__title" comment="Preference group title">Historie emotikonů</string>
<string name="prefs__media__emoji_history_enabled" comment="Preference title">Zapnout historii emotikonů</string>
<string name="prefs__media__emoji_history_enabled__summary" comment="Preference summary">Uchovávat nedávno použité emotikony pro rychlý přístup</string>
<string name="prefs__media__emoji_history_pinned_update_strategy" comment="Preference title">Strategie aktualizace (připnuté)</string>
<string name="prefs__media__emoji_history_recent_update_strategy" comment="Preference title">Strategie aktualizace (nedávné)</string>
<string name="prefs__media__emoji_history_max_size">Maximální počet položek k uchování</string>
<string name="prefs__media__emoji_suggestion__title" comment="Preference group title">Návrhy emotikonů</string>
<string name="prefs__media__emoji_suggestion_enabled" comment="Preference title">Povolit návrhy emotikonů</string>
<string name="prefs__media__emoji_suggestion_enabled__summary" comment="Preference summary">Poskytovat návrhy emotikonů, zatímco píšete</string>
<string name="prefs__media__emoji_suggestion_type" comment="Preference title">Typ spouštěče</string>
<string name="prefs__media__emoji_suggestion_update_history" comment="Preference title">Aktualizovat historii emotikonů</string>
<string name="prefs__media__emoji_suggestion_update_history__summary" comment="Preference summary">Přijetí navržených emotikonů je přidá do historie emotikonů</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name" comment="Preference title">Zobrazit název emotikonu</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name__summary" comment="Preference summary">Návrhy emotikonů zobrazují název vedle emotikonu</string>
<string name="prefs__media__emoji_suggestion_query_min_length" comment="Preference title">Minimální délka textu</string>
<string name="prefs__media__emoji_suggestion_candidate_max_count" comment="Preference title">Maximální počet kandidátů</string>
<!-- Emoji strings -->
<string name="emoji__category__smileys_emotion" comment="Emoji category name">Smajlíky a emotikony</string>
<string name="emoji__category__people_body" comment="Emoji category name">Lidé a tělo</string>
@@ -26,10 +41,12 @@
<string name="emoji__category__objects" comment="Emoji category name">Předměty</string>
<string name="emoji__category__symbols" comment="Emoji category name">Symboly</string>
<string name="emoji__category__flags" comment="Emoji category name">Vlajky</string>
<string name="emoji__recently_used__empty_message" comment="Message if no recently used emojis exist">Nenalezeny žádné nedávno použité emotikony. Jakmile je začnete psát, objeví se automaticky zde.</string>
<string name="emoji__recently_used__phone_locked_message" comment="Message to show if phone is locked">Pro přístup k historii smajlíků nejprve prosím odemkněte své zařízení.</string>
<string name="emoji__recently_used__removal_tip" comment="Feature discoverability for removal of recently used emojis">Tip: Stiskněte a držte prst na naposledy použitých emotikonech pro jejich odebrání z daného seznamu!</string>
<string name="emoji__recently_used__removal_success_message" comment="Toast message if user has long pressed emoji in recently used collection to remove it">Emotikon {emoji} bylo odebráno z nedávno použitých</string>
<string name="emoji__history__empty_message" comment="Message if the emoji history is empty">Nenalezeny žádné nedávno použité emotikony. Jakmile je začnete psát, objeví se automaticky zde.</string>
<string name="emoji__history__phone_locked_message" comment="Message to show if phone is locked">Pro přístup k historii emotikonů nejprve prosím odemkněte své zařízení.</string>
<string name="emoji__history__usage_tip" comment="Feature discoverability for actions of emoji history">Tip: dlouze stiskněte emotikony v historii emotikonů pro jejich připnutí nebo odstranění!</string>
<string name="emoji__history__removal_success_message" comment="Toast message if user has used the delete action on an emoji in the emoji history">Emotikon {emoji} odstraněn z historie</string>
<string name="emoji__history__pinned">Připnuté</string>
<string name="emoji__history__recent">Nedávné</string>
<!-- Quick action strings -->
<string name="quick_action__arrow_up" maxLength="12">Šipka nahoru</string>
<string name="quick_action__arrow_up__tooltip">Vykonat šipku nahoru</string>
@@ -124,21 +141,20 @@
<string name="settings__localization__subtype_error_fields_no_value" comment="Error message shown in subtype editor if at least one field is set to '- select -' (means no value specified)">Minimálně jedno pole nemá vybranou hodnotu. Vyberte ji prosím.</string>
<string name="settings__localization__subtype_error_layout_not_installed" comment="Error message shown in subtype list when a layout is not installed, where %s will be replaced by the layout ID">{layout_id} (nenainstalováno)</string>
<string name="settings__localization__group_layouts__label" comment="Label of layouts group">Rozložení</string>
<string name="settings__localization__subtype_delete_confirmation_title" comment="Title of the subtype delete confirmation dialog">Potvrzení odstranění</string>
<string name="settings__localization__subtype_delete_confirmation_warning" comment="Warning message in the confirmation dialog to confirm the user's intent to delete">Opravdu chcete odstranit tento podtyp?</string>
<string name="settings__theme__title" comment="Title of the Theme screen">Motiv</string>
<string name="pref__theme__mode__label" comment="Label of the theme mode preference">Režim motivu</string>
<string name="pref__theme__sunrise_time__label" comment="Label of the sunrise time preference">Čas východu slunce</string>
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Čas západu slunce</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Denní motiv</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Noční motiv</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Vybraný motiv</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Přizpůsobit barvy aplikaci</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Barvy motivu se přizpůsobí barvám používané aplikace, pokud to daná aplikace podporuje.</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Spravovat nainstalované motivy</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">Podklady aplikace FlorisBoard</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Interní úložiště</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">Externí poskytovatel</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Vyberte denní motiv</string>
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Vyberte noční motiv</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Spravovat nainstalované motivy</string>
<string name="settings__theme_editor__fine_tune__title">Jemné doladění editoru</string>
<string name="settings__theme_editor__fine_tune__level">Úprava úrovně</string>
<string name="settings__theme_editor__fine_tune__display_colors_as">Zobrazit barvy jako</string>
@@ -311,6 +327,7 @@
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">Délka dlouhého stisku</string>
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">Mezerník přepne na znaky</string>
<string name="pref__keyboard__space_bar_switches_to_characters__summary" comment="Preference summary">Automaticky se vrátí z nabídky čísel/symbolů ke klasické klávesnici</string>
<string name="pref__keyboard__incognito_indicator__label" comment="Preference title">Indikátor anonymního režimu</string>
<!-- Smartbar strings -->
<string name="settings__smartbar__title" comment="Title of Smartbar screen">Chytrá lišta</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">Povolit chytrou lištu</string>
@@ -413,6 +430,7 @@
<string name="pref__advanced__settings_theme__light" comment="Possible value of Settings theme preference in Advanced">Světlý</string>
<string name="pref__advanced__settings_theme__dark" comment="Possible value of Settings theme preference in Advanced">Tmavý</string>
<string name="pref__advanced__settings_theme__amoled_dark" comment="Possible value of Settings theme preference in Advanced">AMOLED Temná</string>
<string name="pref__advanced__settings_material_you__label" comment="Label of Material You preference in Advanced">Použít Material You</string>
<string name="pref__advanced__settings_language__label" comment="Label of Settings language preference in Advanced">Jazyk nastavení</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">Zobrazit ikonu aplikace na domovské obrazovce</string>
<string name="pref__advanced__show_app_icon__summary_atleast_q" comment="Summary of Show app icon preference in Advanced for Android 10+">Vždy povoleno na Android 10+ kvůli omezením systému</string>
@@ -472,6 +490,10 @@
<string name="backup_and_restore__back_up__files_ime_keyboard">Rozšíření klávesnice</string>
<string name="backup_and_restore__back_up__files_ime_spelling">Rozšíření kontroly pravopisu / slovníky</string>
<string name="backup_and_restore__back_up__files_ime_theme">Rozšíření témat</string>
<string name="backup_and_restore__back_up__files_clipboard_history">Historie schránky</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_text_items">Textové položky</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_image_items">Obrázky</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_video_items">Videa</string>
<string name="backup_and_restore__back_up__success">Archiv zálohy byl úspěšně exportován!</string>
<string name="backup_and_restore__back_up__failure">Nepodařilo se exportovat archiv zálohy: {error_message}</string>
<string name="backup_and_restore__restore__title">Obnovit data</string>
@@ -560,6 +582,8 @@
<string name="devtools__show_input_state_overlay__summary" comment="Summary of Show input cache overlay in Devtools">Zobrazí aktuální stav vstupu pro ladění</string>
<string name="devtools__show_spelling_overlay__label" comment="Label of Show spelling overlay in Devtools">Zobrazit překrytí s pravopisem</string>
<string name="devtools__show_spelling_overlay__summary" comment="Summary of Show spelling overlay in Devtools">Zobrazí aktuální výsledky pravopisu pro ladění</string>
<string name="devtools__show_inline_autofill_overlay__label">Zobrazit překrytí automatického vyplňování na řádku</string>
<string name="devtools__show_inline_autofill_overlay__summary">Zobrazí aktuální výsledky automatického vyplňování na řádku pro ladění</string>
<string name="devtools__show_key_touch_boundaries__label" comment="Label of Show key touch boundaries in Devtools">Zobrazit hranice dotyku kláves</string>
<string name="devtools__show_key_touch_boundaries__summary" comment="Summary of Show key touch boundaries in Devtools">Zobrazit červené ohraničení hranic dotyku kláves</string>
<string name="devtools__show_drag_and_drop_helpers__label" comment="Label of Show drag and drop helpers in Devtools">Zobrazit pomocníky drag&amp;drop</string>
@@ -575,7 +599,16 @@
<string name="devtools__android_settings_secure__title" comment="Title of Android settings (secure) screen">Bezpečná nastavení Androidu</string>
<string name="devtools__android_settings_system__title" comment="Title of Android settings (system) screen">Systémová nastavení Androidu</string>
<string name="devtools__android_locales__title" comment="Title of Android locales screen">Systémové jazyky</string>
<string name="devtools__debuglog__title">Protokol ladění</string>
<string name="devtools__debuglog__copied_to_clipboard">Protokol ladění zkopírován do schránky</string>
<string name="devtools__debuglog__copy_log">Kopírovat protokol</string>
<string name="devtools__debuglog__copy_for_github">Kopírovat protokol (formátování GitHub)</string>
<string name="devtools__debuglog__loading">Načítání…</string>
<!-- Extension strings -->
<string name="ext__home__title">Doplňky a rozšíření</string>
<string name="ext__list__ext_theme">Rozšíření témat</string>
<string name="ext__list__ext_keyboard">Rozšíření klávesnice</string>
<string name="ext__list__ext_languagepack">Rozšíření jazykových balíčků</string>
<string name="ext__meta__authors">Autoři</string>
<string name="ext__meta__components">Zabalené komponenty</string>
<string name="ext__meta__components_theme">Zabalená témata</string>
@@ -628,6 +661,40 @@
<string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">Byl očekáván soubor médií (obrázek, zvuk, písmo atd.), ale byl nalezen archiv rozšíření.</string>
<string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">Byl očekáván archiv rozšíření, ale byl nalezen soubor médií (obrázek, zvuk, písmo atd.).</string>
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">Při importování se vyskytla chyba. Byly poskytnuty následující podrobnosti:</string>
<string name="ext__validation__enter_package_name">Zadejtze prosím název balíčku</string>
<string name="ext__validation__error_package_name">Název balíčku neodpovídá regexu {id_regex}</string>
<string name="ext__validation__enter_version">Zadejte prosím verzi</string>
<string name="ext__validation__enter_title">Zadejte prosím název</string>
<string name="ext__validation__enter_maintainer">Zadejte prosím alespoň jednoho platného správce</string>
<string name="ext__validation__enter_license">Zadejte prosím identifikátor licence</string>
<string name="ext__validation__enter_component_id">Zadejte prosím ID komponenty</string>
<string name="ext__validation__error_component_id">Zadejte prosím ID komponenty odpovídající {component_id_regex}</string>
<string name="ext__validation__enter_component_label">Zadejte prosím štítek komponenty</string>
<string name="ext__validation__hint_component_label_to_long">Váš štítek komponenty je poměrně dlouhý, což může vést k problémům v rozhraní</string>
<string name="ext__validation__error_author">Zadejte prosím alespoň jednoho platného autora</string>
<string name="ext__validation__error_stylesheet_path_blank">Cesta tabulky stylů nemůže být prázdná</string>
<string name="ext__validation__error_stylesheet_path">Zadejte prosím platnou cestu tabulky stylů odpovídající {stylesheet_path_regex}</string>
<string name="ext__validation__enter_property">Zadejte prosím název proměnné</string>
<string name="ext__validation__error_property">Zadejte prosím platný název proměnné odpovídající {variable_name_regex}</string>
<string name="ext__validation__hint_property" tools:ignore="TypographyDashes">Názvy proměnných ve FlorisCSS obvykle začínají dvěma spojovníky (--)</string>
<string name="ext__validation__enter_color">Zadejte prosím řetězec barvy</string>
<string name="ext__validation__error_color">Zadejte prosím platný řetězec barvy</string>
<string name="ext__validation__enter_dp_size">Zadejte prosím velikost dp</string>
<string name="ext__validation__enter_valid_number">Zadejte prosím platné číslo</string>
<string name="ext__validation__enter_positive_number">Zadejte prosím kladné číslo (&gt;=0)</string>
<string name="ext__validation__enter_percent_size">Zadejte prosím velikost v procentech</string>
<string name="ext__validation__enter_number_between_0_100">Zadejte prosím kladné číslo mezi 0 a 100</string>
<string name="ext__validation__hint_value_above_50_percent">Jakákoli hodnota nad 50 % se bude chovat, jako kdybyste nastavili 50 %, zvažte snížení velikosti v procentech</string>
<string name="ext__update_box__internet_permission_hint">Jelikož tato aplikace nemá přístup k internetu, musíte aktualizace nainstalovaných rozšíření kontrolovat ručně.</string>
<string name="ext__update_box__search_for_updates">Vyhledat aktualizace</string>
<string name="ext__addon_management_box__managing_placeholder">Správa {extensions}</string>
<string name="ext__addon_management_box__addon_manager_info">Všechny úlohy spojené s importováním, exportováním, vytvářením, přizpůsobováním a odstraňováním rozšíření mohou být prováděny skrze centralizovaného správce doplňků.</string>
<string name="ext__addon_management_box__go_to_page">Přejít na {ext_home_title}</string>
<string name="ext__home__info">Rozšíření můžete stahovat a instalovat z Obchodu s doplňky FlorisBoard nebo importovat jakýkoli soubor rozšíření, který jste si stáhli z internetu.</string>
<string name="ext__home__visit_store">Navštívit Obchod s doplňky</string>
<string name="ext__home__manage_extensions">Správa nainstalovaných doplňků</string>
<string name="ext__list__view_details">Zobrazit podrobnosti</string>
<string name="ext__check_updates__title">Zkontrolovat aktualizace</string>
<!-- Action strings -->
<string name="action__add">Přidat</string>
<string name="action__apply">Použít</string>
@@ -695,6 +762,14 @@
<string name="enum__display_language_names_in__system_locale__description" comment="Enum value description">Názvy jazyků napříč aplikací a rozhraním klávesnice jsou zobrazeny v jazyce, který je nastaven pro celé zařízení</string>
<string name="enum__display_language_names_in__native_locale" comment="Enum value label">Nativní jazyk</string>
<string name="enum__display_language_names_in__native_locale__description" comment="Enum value description">Názvy jazyků napříč aplikací a rozhraním klávesnice jsou zobrazeny ve svém vlastním jazyce</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend" comment="Enum value label">Automatické řazení (na začátku)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend__description" comment="Enum value description">Automaticky seřazovat emotikony podle jejich používání. Nové emotikony budou přidávány na začátek.</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append" comment="Enum value label">Automatické řazení (na konci)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append__description" comment="Enum value description">Automaticky seřazovat emotikony podle jejich používání. Nové emotikony budou přidávány na konec.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend" comment="Enum value label">Ruční řazení (na začátku)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend__description" comment="Enum value description">Neseřazovat automaticky emotikony podle jejich používání. Nové emotikony budou přidávány na začátek.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append" comment="Enum value label">Ruční řazení (na konci)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append__description" comment="Enum value description">Neseřazovat automaticky emotikony podle jejich používání. Nové emotikony budou přidávány na konec.</string>
<string name="enum__emoji_skin_tone__default" comment="Enum value label">{emoji} Výchozí barva pleti</string>
<string name="enum__emoji_skin_tone__light_skin_tone" comment="Enum value label">{emoji} Světlá barva pleti</string>
<string name="enum__emoji_skin_tone__medium_light_skin_tone" comment="Enum value label">{emoji} Středně světlá barva pleti</string>
@@ -706,9 +781,13 @@
<string name="enum__emoji_hair_style__curly_hair" comment="Enum value label">{emoji} Kurdnaté vlasy</string>
<string name="enum__emoji_hair_style__white_hair" comment="Enum value label">{emoji} Bílé vlasy</string>
<string name="enum__emoji_hair_style__bald" comment="Enum value label">{emoji} Pleš</string>
<string name="enum__emoji_suggestion_type__leading_colon">Počáteční dvojtečka</string>
<string name="enum__emoji_suggestion_type__leading_colon__description" comment="Keep the :emoji_name while translating, this is a syntax guide">Navrhovat emotikony pomocí syntaxe :emoji_name</string>
<string name="enum__emoji_suggestion_type__inline_text">Text v řádku</string>
<string name="enum__emoji_suggestion_type__inline_text__description">Navrhovat emotikony jednoduše zadáním názvu emotikonu jako slova</string>
<string name="enum__extended_actions_placement__above_candidates" comment="Enum value label">Nad navrženými slovy</string>
<string name="enum__extended_actions_placement__above_candidates__description" comment="Enum value description">Umístí řádek rozšířených akcí mezi UI aplikace a řádek navrhnutých slov</string>
<string name="enum__extended_actions_placement__below_candidates" comment="Enum value label">Pod navrhnutými slovy</string>
<string name="enum__extended_actions_placement__below_candidates" comment="Enum value label">Pod navrženými slovy</string>
<string name="enum__extended_actions_placement__below_candidates__description" comment="Enum value description">Umístí řádek rozšířených akcí mezi řádek s navrhnutými slovy a textovou klávesnici</string>
<string name="enum__extended_actions_placement__overlay_app_ui" comment="Enum value label">Překrýt rozhraní aplikace</string>
<string name="enum__extended_actions_placement__overlay_app_ui__description" comment="Enum value description">Umístí řádek rozšířených akcí jako překrytí nad UI aplikace, bez ovlivnění výšky rozhraní klávesnice. Vezměte prosím na vědomí, že toto umístění může způsobit částečné překreslení vstupního pole aplikace</string>
@@ -722,6 +801,8 @@
<string name="enum__key_hint_mode__hint_priority__description" comment="Enum value description">Počáteční znak vybraný po dlouhém stisku je vždy znak nápovědy, nebo primární přízvuk, když není dostupný symbol nápovědy</string>
<string name="enum__key_hint_mode__smart_priority" comment="Enum value label">Chytrá prioritizace</string>
<string name="enum__key_hint_mode__smart_priority__description" comment="Enum value description">Počáteční znak vybraný po dlouhém stisku je dynamicky vybrán, zda bude primárním přízvukem, nebo o symbolem nápovědy, a to na základě aktuálního jazyka a rozložení</string>
<string name="enum__incognito_display_mode__replace_shared_actions_toggle" comment="Enum value label">Nahradit ikonu přepínače sdílených akcí indikátorem anonymního režimu</string>
<string name="enum__incognito_display_mode__display_behind_keyboard" comment="Enum value label">Zobrazit indikátor anonymního režimu v pozadí klávesnice</string>
<string name="enum__incognito_mode__force_off" comment="Enum value label">Vynutit vypnutí</string>
<string name="enum__incognito_mode__force_off__description" comment="Enum value description">Anonymní režim bude vždy zakázán, nehledě na požadavky cílové aplikace. S touto možností nebude v chytré liště zobrazena rychlá akce anonymního režimu.</string>
<string name="enum__incognito_mode__force_on" comment="Enum value label">Vynutit zapnutí</string>
@@ -827,4 +908,16 @@
<item quantity="many">{v} položek</item>
<item quantity="other">{v} položek</item>
</plurals>
<plurals name="unit__characters__written">
<item quantity="one">{v} znak</item>
<item quantity="few">{v} znaky</item>
<item quantity="many">{v} znaků</item>
<item quantity="other">{v} znaků</item>
</plurals>
<plurals name="unit__candidates__written">
<item quantity="one">{v} návrh</item>
<item quantity="few">{v} návrhy</item>
<item quantity="many">{v} návrhů</item>
<item quantity="other">{v} návrhů</item>
</plurals>
</resources>

View File

@@ -9,9 +9,12 @@
<string name="one_handed__move_start_btn_content_description" comment="Content description for the one-handed move to left button">Flyt tastatur til venstre.</string>
<string name="one_handed__move_end_btn_content_description" comment="Content description for the one-handed move to right button">Flyt tastatur til højre.</string>
<!-- Media strings -->
<string name="settings__media__title">Emojis</string>
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">Emojis</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">Humørikoner</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">Tekst-emoji</string>
<string name="prefs__media__emoji_preferred_skin_tone">Foretrukken emoji-hudfarve</string>
<string name="prefs__media__emoji_preferred_hair_style">Foretrukken emoji-frisure</string>
<!-- Emoji strings -->
<string name="emoji__category__smileys_emotion" comment="Emoji category name">Smileys &amp; Følelser</string>
<string name="emoji__category__people_body" comment="Emoji category name">Personer &amp; Krop</string>
@@ -23,23 +26,55 @@
<string name="emoji__category__symbols" comment="Emoji category name">Symboler</string>
<string name="emoji__category__flags" comment="Emoji category name">Flag</string>
<!-- Quick action strings -->
<string name="quick_action__arrow_up" maxLength="12">Pil op</string>
<string name="quick_action__arrow_up__tooltip">Udfør pil op</string>
<string name="quick_action__arrow_down" maxLength="12">Pil ned</string>
<string name="quick_action__arrow_down__tooltip">Udfør pil ned</string>
<string name="quick_action__arrow_left" maxLength="12">Pil venstre</string>
<string name="quick_action__arrow_left__tooltip">Udfør pil venstre</string>
<string name="quick_action__arrow_right" maxLength="12">Pil højre</string>
<string name="quick_action__arrow_right__tooltip">Udfør pil højre</string>
<string name="quick_action__clipboard_clear_primary_clip__tooltip">Udfør ryd primær udklipsholder</string>
<string name="quick_action__clipboard_copy" maxLength="12">Kopiér</string>
<string name="quick_action__clipboard_cut" maxLength="12">Klip</string>
<string name="quick_action__clipboard_paste" maxLength="12">Indsæt</string>
<string name="quick_action__clipboard_select_all" maxLength="12">Vælg alle</string>
<string name="quick_action__ime_ui_mode_media" maxLength="12">Emoji</string>
<string name="quick_action__ime_ui_mode_media__tooltip">Åbn emoji-panel</string>
<string name="quick_action__settings__tooltip">Åbn indstillinger</string>
<string name="quick_action__undo" maxLength="12">Fortryd</string>
<string name="quick_action__toggle_incognito_mode" maxLength="12">Inkognito</string>
<string name="quick_action__toggle_incognito_mode__tooltip">Slå inkognito-tilstand til/fra</string>
<string name="quick_action__voice_input" maxLength="12">Stemmeinput</string>
<string name="quick_action__voice_input__tooltip" comment="IME stands for Input Method Editor and is indirectly equivalent to 'keyboard'.">Åbn stemmeinput-udbyder</string>
<string name="quick_action__one_handed_mode__tooltip">Skift enhåndstilstand</string>
<string name="quick_action__drag_marker__tooltip" comment="This action is only used as a placeholder in the actions editor drag and drop screen and only visible in debug mode">Nuværende træk-markør position</string>
<string name="quick_action__noop" maxLength="12" comment="Noop=no operation; this action is only used as a placeholder in the actions editor drag and drop screen">Ingen</string>
<!-- Incognito mode strings -->
<!-- Settings UI strings -->
<string name="settings__title" comment="Title of Settings">Indstillinger</string>
<string name="settings__preview_keyboard" comment="Hint for try your setup box">Prøv din opsætning</string>
<string name="settings__help" comment="General label for help buttons in Settings">Hjælp</string>
<string name="settings__default" comment="General string which is used when a preference has the default value set">Standard</string>
<string name="settings__system_default" comment="General string which is used when a preference has the system default value set">Systemstandard</string>
<string name="settings__home__title" comment="Title of the Home screen">Velkommen til {app_name}</string>
<string name="settings__home__ime_not_enabled" comment="Error message shown in Home fragment when FlorisBoard is not enabled in the system">FlorisBoard er ikke slået til i systemet og er derfor ikke tilgængelig som inputsmetode i inputvælgeren. Klik her for at løse problemet.</string>
<string name="settings__home__ime_not_selected" comment="Warning message shown in Home fragment when FlorisBoard is not selected as the default keyboard">FlorisBoard er ikke valgt som standard inputmetode. Klik her for at løse problemet.</string>
<string name="settings__localization__title" comment="Title of languages and Layout screen">Sprog &amp; Layouts</string>
<string name="settings__localization__display_language_names_in__label" comment="Label of Display language names in preference">Vis sprognavne i</string>
<string name="settings__localization__group_subtypes__label" comment="Label of subtypes group">Undertyper</string>
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">Tilføj undertastatur</string>
<string name="settings__localization__language_pack_title" comment="Title of the language pack manager screen for managing installed and custom language packs">Administrér installerede sprogpakker</string>
<string name="settings__localization__subtype_edit_title" comment="Title of subtype dialog when editing an existing subtype">Ændre undertastatur</string>
<string name="settings__localization__subtype_locale" comment="Label for locale dropdown in subtype dialog">Primært sprog</string>
<string name="settings__localization__subtype_suggestion_provider" comment="Label for suggestion provider dropdown in subtype dialog">Inkognito-tilstand er nu aktiveret. Vil ikke lærer ord fra dine input mens denne tilstand er aktiv</string>
<string name="settings__localization__subtype_select_locale" comment="Subtype select language title">Vælg sprog</string>
<string name="settings__localization__subtype_no_subtypes_configured_warning" comment="Warning message that no subtype has been defined">Det ser ud til, at der ikke er konfigureret nogle undertastaturer. I dette tilfælde faldes der tilbage på Engelsk/QWERTY!</string>
<string name="settings__localization__subtype_error_already_exists" comment="Error message shown in subtype dialog when a subtype to add already exists">Dette undertastatur findes allerede!</string>
<string name="settings__localization__subtype_error_layout_not_installed" comment="Error message shown in subtype list when a layout is not installed, where %s will be replaced by the layout ID">{layout_id} (ikke installeret)</string>
<string name="settings__theme__title" comment="Title of the Theme screen">Tema</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Valgte tema</string>
<string name="settings__theme_editor__rule_selectors"></string>
<string name="snygg__rule_element__keyboard">Tastatur vindue</string>
<string name="pref__input_feedback__audio_enabled__label" comment="Preference title">Aktivér lydfeedback</string>
<string name="pref__input_feedback__haptic_vibration_duration__label" comment="Preference title">Vibrationsvarighed</string>
<string name="pref__input_feedback__haptic_vibration_strength__label" comment="Preference title">Vibrationsstyrke</string>

View File

@@ -13,9 +13,24 @@
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">Emojis</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">Emoticons</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">Kaomoji</string>
<string name="prefs__media__emoji_recently_used_max_size">Maximale Größe des Emoji-Verlaufs</string>
<string name="prefs__media__emoji_preferred_skin_tone">Bevorzugte Emoji-Hautfarbe</string>
<string name="prefs__media__emoji_preferred_hair_style">Bevorzugte Emoji-Haarfarbe</string>
<string name="prefs__media__emoji_history__title" comment="Preference group title">Emoji-Historie</string>
<string name="prefs__media__emoji_history_enabled" comment="Preference title">Aktiviere Emoji-Historie</string>
<string name="prefs__media__emoji_history_enabled__summary" comment="Preference summary">Behalte kürzlich genutzte Emojis für einen schnellen Zugriff</string>
<string name="prefs__media__emoji_history_pinned_update_strategy" comment="Preference title">Update Strategie (gepinnt)</string>
<string name="prefs__media__emoji_history_recent_update_strategy" comment="Preference title">Update Strategie (kürzlich)</string>
<string name="prefs__media__emoji_history_max_size">Maximale zu behaltende Emojis</string>
<string name="prefs__media__emoji_suggestion__title" comment="Preference group title">Emoji Vorschläge</string>
<string name="prefs__media__emoji_suggestion_enabled" comment="Preference title">Aktiviere Emoji Vorschläge</string>
<string name="prefs__media__emoji_suggestion_enabled__summary" comment="Preference summary">Erhalte Emoji Vorschläge während des Tippens</string>
<string name="prefs__media__emoji_suggestion_type" comment="Preference title">Auslösetyp</string>
<string name="prefs__media__emoji_suggestion_update_history" comment="Preference title">Update Emoji-Historie</string>
<string name="prefs__media__emoji_suggestion_update_history__summary" comment="Preference summary">Angenommene vorgeschlagene Emojis werden zur Emoji-Historie hinzugefügt</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name" comment="Preference title">Zeige Emoji Name</string>
<string name="prefs__media__emoji_suggestion_candidate_show_name__summary" comment="Preference summary">Emoji Vorschläge zeigen den Namen neben dem Emoji</string>
<string name="prefs__media__emoji_suggestion_query_min_length" comment="Preference title">Minimale Suchbegriff-Länge</string>
<string name="prefs__media__emoji_suggestion_candidate_max_count" comment="Preference title">Maximale Anzahl von Vorschlägen</string>
<!-- Emoji strings -->
<string name="emoji__category__smileys_emotion" comment="Emoji category name">Smileys &amp; Emotionen</string>
<string name="emoji__category__people_body" comment="Emoji category name">Personen &amp; Körper</string>
@@ -26,10 +41,12 @@
<string name="emoji__category__objects" comment="Emoji category name">Objekte</string>
<string name="emoji__category__symbols" comment="Emoji category name">Symbole</string>
<string name="emoji__category__flags" comment="Emoji category name">Flaggen</string>
<string name="emoji__recently_used__empty_message" comment="Message if no recently used emojis exist">Keine kürzlich verwendeten Emojis gefunden. Sobald Sie Emojis verwendet haben erscheinen diese hier.</string>
<string name="emoji__recently_used__phone_locked_message" comment="Message to show if phone is locked">Um auf deinen Emoji-verlauf zuzugreifen, entsperre zuerst dein Gerät.</string>
<string name="emoji__recently_used__removal_tip" comment="Feature discoverability for removal of recently used emojis">Pro Tipp: Halten Sie kürzlich verwendete Emojis gedrückt um diese wieder aus der Ansicht zu entfernen!</string>
<string name="emoji__recently_used__removal_success_message" comment="Toast message if user has long pressed emoji in recently used collection to remove it">{emoji} wurde aus \"kürzlich verwendeten Emojis\" entfernt</string>
<string name="emoji__history__empty_message" comment="Message if the emoji history is empty">Keine kürzlich benutzen Emojis gefunden. Sobald Emojis benutzt werden, erscheinen diese hier.</string>
<string name="emoji__history__phone_locked_message" comment="Message to show if phone is locked">Um auf deinen Emoji-Historie zuzugreifen, entsperre das Gerät.</string>
<string name="emoji__history__usage_tip" comment="Feature discoverability for actions of emoji history">Tipp: Tippe lange auf Emojis in der Historie um sie anzupinnen oder zu entfernen!</string>
<string name="emoji__history__removal_success_message" comment="Toast message if user has used the delete action on an emoji in the emoji history">{emoji} aus Historie gelöscht</string>
<string name="emoji__history__pinned">Angepinnt</string>
<string name="emoji__history__recent">Kürzlich</string>
<!-- Quick action strings -->
<string name="quick_action__arrow_up" maxLength="12">Pfeil hoch</string>
<string name="quick_action__arrow_up__tooltip">Pfeil nach oben ausführen</string>
@@ -46,13 +63,13 @@
<string name="quick_action__clipboard_cut" maxLength="12">Ausschneiden</string>
<string name="quick_action__clipboard_cut__tooltip">Zur Zwischenablage ausschneiden</string>
<string name="quick_action__clipboard_paste" maxLength="12">Einfügen</string>
<string name="quick_action__clipboard_paste__tooltip">Einfügen von der Zwischenablage</string>
<string name="quick_action__clipboard_paste__tooltip">Einfügen aus der Zwischenablage</string>
<string name="quick_action__clipboard_select_all" maxLength="12">Alles ausw.</string>
<string name="quick_action__clipboard_select_all__tooltip">Alles auswählen in der Zwischenablage</string>
<string name="quick_action__ime_ui_mode_clipboard" maxLength="12">Zwischenab.</string>
<string name="quick_action__ime_ui_mode_clipboard__tooltip">Zwischenablage Verlauf öffnen</string>
<string name="quick_action__ime_ui_mode_media" maxLength="12">Emoji</string>
<string name="quick_action__ime_ui_mode_media__tooltip">Öffne Emoji-Palette</string>
<string name="quick_action__ime_ui_mode_media__tooltip">Öffne Emoji-Panel</string>
<string name="quick_action__settings" maxLength="12">Einstellung</string>
<string name="quick_action__settings__tooltip">Einstellungen öffnen</string>
<string name="quick_action__undo" maxLength="12">Rückgängig</string>
@@ -71,7 +88,7 @@
<string name="quick_action__one_handed_mode__tooltip">Einhandmodus umschalten</string>
<string name="quick_action__drag_marker" maxLength="12" comment="This action is only used as a placeholder in the actions editor drag and drop screen and only visible in debug mode">Zug-Marker</string>
<string name="quick_action__drag_marker__tooltip" comment="This action is only used as a placeholder in the actions editor drag and drop screen and only visible in debug mode">Aktuelle Marker-Position</string>
<string name="quick_action__noop" maxLength="12" comment="Noop=no operation; this action is only used as a placeholder in the actions editor drag and drop screen">Kein</string>
<string name="quick_action__noop" maxLength="12" comment="Noop=no operation; this action is only used as a placeholder in the actions editor drag and drop screen">Keine</string>
<string name="quick_action__noop__tooltip" comment="Noop=no operation; this action is only used as a placeholder in the actions editor drag and drop screen">Keine Operation</string>
<string name="quick_actions_overflow__customize_actions_button">Aktionen neu ordnen</string>
<string name="quick_actions_editor__header">Passe Reihenfolge der Aktionen an</string>
@@ -104,8 +121,8 @@
<string name="settings__localization__subtype_symbols_layout" comment="Label for layout dropdown in subtype dialog">Primäre Symbolanordnung</string>
<string name="settings__localization__subtype_symbols2_layout" comment="Label for layout dropdown in subtype dialog">Sekundäre Symbolanordnung</string>
<string name="settings__localization__subtype_composer" comment="Label for composer dropdown in subtype dialog.">Verfasser</string>
<string name="settings__localization__subtype_currency_set" comment="Label for currency set dropdown in subtype dialog. 'set' is used as a noun here and can be compared to a group of elements (in this case currency symbols).">Währung setzen</string>
<string name="settings__localization__subtype_numeric_layout" comment="Label for layout dropdown in subtype dialog">Numerische Anordnung</string>
<string name="settings__localization__subtype_currency_set" comment="Label for currency set dropdown in subtype dialog. 'set' is used as a noun here and can be compared to a group of elements (in this case currency symbols).">Währungssatz</string>
<string name="settings__localization__subtype_numeric_layout" comment="Label for layout dropdown in subtype dialog">Zahlenlayout</string>
<string name="settings__localization__subtype_numeric_advanced_layout" comment="Label for layout dropdown in subtype dialog">(Erweiterte) Numerische Anordnung</string>
<string name="settings__localization__subtype_numeric_row_layout" comment="Label for layout dropdown in subtype dialog">Anordnung Zahlenreihe</string>
<string name="settings__localization__subtype_phone_layout" comment="Label for layout dropdown in subtype dialog">Primäres Wählfeld</string>
@@ -124,21 +141,20 @@
<string name="settings__localization__subtype_error_fields_no_value" comment="Error message shown in subtype editor if at least one field is set to '- select -' (means no value specified)">In mindestens einem Feld ist kein Wert ausgewählt. Bitte wähle einen Wert für das Feld / die Felder.</string>
<string name="settings__localization__subtype_error_layout_not_installed" comment="Error message shown in subtype list when a layout is not installed, where %s will be replaced by the layout ID">{layout_id} (nicht installiert)</string>
<string name="settings__localization__group_layouts__label" comment="Label of layouts group">Layouts</string>
<string name="settings__localization__subtype_delete_confirmation_title" comment="Title of the subtype delete confirmation dialog">Löschen bestätigen</string>
<string name="settings__localization__subtype_delete_confirmation_warning" comment="Warning message in the confirmation dialog to confirm the user's intent to delete">Bist du sicher, dass du diesen Untertyp löschen willst?</string>
<string name="settings__theme__title" comment="Title of the Theme screen">Design</string>
<string name="pref__theme__mode__label" comment="Label of the theme mode preference">Design-Modus</string>
<string name="pref__theme__sunrise_time__label" comment="Label of the sunrise time preference">Sonnenaufgangszeit</string>
<string name="pref__theme__sunrise_time__label" comment="Label of the sunrise time preference">Sonnenuntergangszeit</string>
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Sonnenuntergangszeit</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Helles Design</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Dunkles Design</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Ausgewähltes Design</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Farbdesign an eine App anpassen</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Farbdesign passt sich an die verwendeten App an. Nur möglich wenn die verwendete App diese Funktionalität unterstützt.</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Installierte Designs verwalten</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">FlorisBoard App Ressourcen</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Interner Speicher</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">Externer Anbieter</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Tages-Design auswählen</string>
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Nacht-Design auswählen</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Installierte Designs verwalten</string>
<string name="settings__theme_editor__fine_tune__title">Editoreinstellungen</string>
<string name="settings__theme_editor__fine_tune__level">Bearbeitungsmodus</string>
<string name="settings__theme_editor__fine_tune__display_colors_as">Zeige Farben als</string>
@@ -163,7 +179,7 @@
<string name="settings__theme_editor__code_recording_help_text">Um den Code einer Taste zu ermitteln, verwenden Sie die Schaltfläche neben dem Code-Eingabefeld. Sobald sie aktiviert ist, zeichnet sie den nächsten Tastendruck auf und fügt den Code in das Eingabefeld ein.</string>
<string name="settings__theme_editor__code_recording_started">Key code aufnahme gestartet</string>
<string name="settings__theme_editor__code_recording_stopped">Key code aufnahme beendet</string>
<string name="settings__theme_editor__code_recording_requires_default_ime_floris">{app_name} muss die standard Tastatur-App sein, um eine Taste aufzunehmen</string>
<string name="settings__theme_editor__code_recording_requires_default_ime_floris">{app_name} muss die Standard-Tastatur-App sein, um eine Taste aufzunehmen</string>
<string name="settings__theme_editor__code_recording_placeholder">Aufnahme läuft …</string>
<string name="settings__theme_editor__add_property">Eigenschaft hinzufügen</string>
<string name="settings__theme_editor__edit_property">Eigenschaft bearbeiten</string>
@@ -311,6 +327,7 @@
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">Verzögerung bei langem Tastendruck</string>
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">Leertaste wechselt zu Buchstaben zurück</string>
<string name="pref__keyboard__space_bar_switches_to_characters__summary" comment="Preference summary">Leertaste wechselt aus der Symbol- oder Zahlenansicht zurück zur Buchstabenansicht</string>
<string name="pref__keyboard__incognito_indicator__label" comment="Preference title">Inkognito-Indikator</string>
<!-- Smartbar strings -->
<string name="settings__smartbar__title" comment="Title of Smartbar screen">Schnellzugriffsleiste</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">Schnellzugriffsleiste einschalten</string>
@@ -380,7 +397,7 @@
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Abkürzung (optional)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Bitte einen Shortcut eingeben, der zu {regex} passt</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Sprachcode (optional)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Dieses Sprachcode entspicht nicht denn enstpächende Syntax. Der Code muss entsprechend von Land (wie en), Land und Region (wie en_US) oder Land, Region und Script (wie en_US-script).</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Dieser Sprachcode entspricht nicht der Syntax. Der Code muss entweder ein Land (wie en), Land und Region (wie en_US) oder Land, Region und Script (wie en_US-script) sein.</string>
<string name="settings__gestures__title" comment="Title of Gestures screen">Gesten &amp; Glide Typing</string>
<string name="pref__glide__title" comment="Preference group title">Glide Typing</string>
<string name="pref__glide__enabled__label" comment="Preference title">Glide Typing aktivieren</string>
@@ -413,6 +430,7 @@
<string name="pref__advanced__settings_theme__light" comment="Possible value of Settings theme preference in Advanced">Hell</string>
<string name="pref__advanced__settings_theme__dark" comment="Possible value of Settings theme preference in Advanced">Dunkel</string>
<string name="pref__advanced__settings_theme__amoled_dark" comment="Possible value of Settings theme preference in Advanced">AMOLED Dunkel</string>
<string name="pref__advanced__settings_material_you__label" comment="Label of Material You preference in Advanced">Nutze Material You</string>
<string name="pref__advanced__settings_language__label" comment="Label of Settings language preference in Advanced">App-Sprache</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">Zeige die App in der Übersicht</string>
<string name="pref__advanced__show_app_icon__summary_atleast_q" comment="Summary of Show app icon preference in Advanced for Android 10+">Immer aktiviert in Android 10+ aufgrund von System-Beschränkungen</string>
@@ -434,7 +452,7 @@
<string name="about__privacy_policy__title" comment="Preference title">Datenschutzrichtlinie</string>
<string name="about__privacy_policy__summary" comment="Preference summary">Datenschutzrichtlinie für dieses Projekt</string>
<string name="about__project_license__title" comment="Preference title">Projekt-Lizenz</string>
<string name="about__project_license__summary" comment="Preference summary">FlorisBoard ist unter {license_name} lizensiert</string>
<string name="about__project_license__summary" comment="Preference summary">FlorisBoard ist unter {license_name} lizenziert</string>
<string name="about__project_license__error_license_text_failed" comment="Error text for license text loading failure">Fehler: Lizenztext konnte nicht geladen werden.\n-&gt; Grund: {error_message}</string>
<string name="about__project_license__error_reason_asset_manager_null" comment="Error text if asset manager is null">Assetmanager Referenz ist null</string>
<string name="about__third_party_licenses__title" comment="Preference title">Drittanbieter-Lizenzen</string>
@@ -450,6 +468,8 @@
<string name="setup__select_ime__title">{app_name} auswählen</string>
<string name="setup__select_ime__description">{app_name} ist nun auf deinem System aktiviert. Um es zu benutzen, wähle bei der Standard-Eingabemethode {app_name} aus!</string>
<string name="setup__select_ime__switch_keyboard_btn">Tastatur wechseln</string>
<string name="setup__grant_notification_permission__title">Fehlerberichts-Benachrichtigungen zulassen</string>
<string name="setup__grant_notification_permission__description">Ab Android 13+ müssen Apps um Erlaubnis bitten, um Benachrichtigungen zu senden. FlorisBoard verwendet diese nur, um im Falle eines App-Absturzes einen Fehlerbericht zu senden. Diese Berechtigung kann jederzeit in den Systemeinstellungen widerrufen werden. </string>
<string name="setup__grant_notification_permission__btn">Berechtigung erteilen</string>
<string name="setup__finish_up__title">Fertigstellen</string>
<string name="setup__finish_up__description_p1">{app_name} ist nun im System aktiviert und bereit von dir angepasst zu werden.</string>
@@ -467,6 +487,10 @@
<string name="backup_and_restore__back_up__files_ime_keyboard">Tastaturerweiterungen</string>
<string name="backup_and_restore__back_up__files_ime_spelling">Rechtschreibeerweiterungen / Wörterbücher</string>
<string name="backup_and_restore__back_up__files_ime_theme">Design-Erweiterungen</string>
<string name="backup_and_restore__back_up__files_clipboard_history">Zwischenablageverlauf</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_text_items">Text Inhalte</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_image_items">Bilder</string>
<string name="backup_and_restore__back_up__files_clipboard_history__clipboard_video_items">Videos</string>
<string name="backup_and_restore__back_up__success">Sicherung wurde erfolgreich exportiert!</string>
<string name="backup_and_restore__back_up__failure">Exportieren des Sicherungsarchivs fehlgeschlagen: {error_message}</string>
<string name="backup_and_restore__restore__title">Daten wiederherstellen</string>
@@ -540,6 +564,8 @@
<string name="pref__clipboard__clear_primary_clip_deletes_last_item__summary">Durch das Löschen des aktiven Elements wird auch das neueste Verlaufselement entfernt</string>
<string name="send_to_clipboard__unknown_error">Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut!</string>
<string name="send_to_clipboard__type_not_supported_error">Dieser Media-Typ wird nicht unterstützt.</string>
<string name="send_to_clipboard__android_version_to_old_error">Die Androidversion ist zu alt für diese Funktion. </string>
<string name="send_to_clipboard__description__copied_image_to_clipboard">Nachfolgendes Bild wurde in die Zwischenablage kopiert.</string>
<!-- Devtools strings -->
<string name="devtools__title" comment="Title of Devtools screen. Translators: treat this string as 'Developer tools' for translation, except a similar short term is available for your language.">Entwicklerwerkzeuge</string>
<string name="devtools__enabled__label" comment="Label of Enable developer tools in Devtools">Entwickler-Werkzeuge ein/aus</string>
@@ -567,7 +593,16 @@
<string name="devtools__android_settings_secure__title" comment="Title of Android settings (secure) screen">Sichere Android-Einstellungen</string>
<string name="devtools__android_settings_system__title" comment="Title of Android settings (system) screen">Android System-Einstellungen</string>
<string name="devtools__android_locales__title" comment="Title of Android locales screen">Systemgebietsschemata</string>
<string name="devtools__debuglog__title">Debug Log</string>
<string name="devtools__debuglog__copied_to_clipboard">Debug Log wurde in die Zwischenablage kopiert</string>
<string name="devtools__debuglog__copy_log">Log kopieren</string>
<string name="devtools__debuglog__copy_for_github">Log kopieren (GitHub Formatierung)</string>
<string name="devtools__debuglog__loading">Lädt…</string>
<!-- Extension strings -->
<string name="ext__home__title">Add-ons &amp; Erweiterungen</string>
<string name="ext__list__ext_theme">Design-Erweiterungen</string>
<string name="ext__list__ext_keyboard">Tastatur-Erweiterungen</string>
<string name="ext__list__ext_languagepack">Sprachpaket-Erweiterungen</string>
<string name="ext__meta__authors">Ersteller</string>
<string name="ext__meta__components">Gebündelte Komponenten</string>
<string name="ext__meta__components_theme">Enthaltene Designs</string>
@@ -620,6 +655,40 @@
<string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">Eine Mediendatei (Bild, Audio, Text, etc.) wurde erwartet, aber ein Erweiterungsarchiv wurde gefunden.</string>
<string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">Ein Erweiterungsarchiv wurde erwartet, aber eine Mediendatei (Bild, Audio, Text, etc.) wurde gefunden.</string>
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">Beim Import ist ein unerwarteter Fehler aufgetreten. Folgende Angaben wurden gemacht:</string>
<string name="ext__validation__enter_package_name">Bitte gib einen Paketnamen ein</string>
<string name="ext__validation__error_package_name">Paketname stimmt nicht mit Regex {id_regex} überein</string>
<string name="ext__validation__enter_version">Bitte gib eine Version an</string>
<string name="ext__validation__enter_title">Bitte gib einen Titel ein</string>
<string name="ext__validation__enter_maintainer">Bitte gib mindestens einen gültigen Maintainer an</string>
<string name="ext__validation__enter_license">Bitte gib eine Lizenzbezeichnung an</string>
<string name="ext__validation__enter_component_id">Bitte gib eine Komponenten-ID ein</string>
<string name="ext__validation__error_component_id">Bitte eine Komponenten-ID eingeben, welche zu {component_id_regex} passt</string>
<string name="ext__validation__enter_component_label">Bitte gib eine Komponentenbezeichnung ein</string>
<string name="ext__validation__hint_component_label_to_long">Die Komponentenbezeichnung ist recht lang, was zu einem Abschneiden auf der Benutzeroberfläche führen kann</string>
<string name="ext__validation__error_author">Bitte gib mindestens einen gültigen Autor an</string>
<string name="ext__validation__error_stylesheet_path_blank">Der Stylesheet-Pfad darf nicht leer sein</string>
<string name="ext__validation__error_stylesheet_path">Bitte einen Stylesheet-Pfad eingeben, welcher zu {stylesheet_path_regex} passt</string>
<string name="ext__validation__enter_property">Bitte gib einen Variablennamen an</string>
<string name="ext__validation__error_property">Bitte einen gültigen Variablennamen eingeben, welcher zu {variable_name_regex} passt</string>
<string name="ext__validation__hint_property" tools:ignore="TypographyDashes">Nach FlorisCSS-Konvention beginnt ein Variablenname mit zwei Bindestrichen (--)</string>
<string name="ext__validation__enter_color">Bitte gib einen Farbstring ein</string>
<string name="ext__validation__error_color">Bitte gib einen gültigen Farbstring ein</string>
<string name="ext__validation__enter_dp_size">Bitte gib eine dp-Größe an</string>
<string name="ext__validation__enter_valid_number">Bitte gib eine gültige Nummer ein</string>
<string name="ext__validation__enter_positive_number">Bitte gib eine positive Zahl ein (&gt;=0)</string>
<string name="ext__validation__enter_percent_size">Bitte gib eine Prozentzahl an</string>
<string name="ext__validation__enter_number_between_0_100">Bitte gib eine positive Zahl zwischen 0 und 100 an</string>
<string name="ext__validation__hint_value_above_50_percent">Jeder Wert über 50 % verhält sich so, als ob 50 % eingestellt wären. Überlege den Wert zu senken</string>
<string name="ext__update_box__internet_permission_hint">Weil diese App keine Internetberechtigungen hat, muss manuell nach Updates für Erweiterungen gesucht werden.</string>
<string name="ext__update_box__search_for_updates">Nach Updates suchen</string>
<string name="ext__addon_management_box__managing_placeholder">Verwalte {extensions}</string>
<string name="ext__addon_management_box__addon_manager_info">Alle Aufgaben im Zusammenhang mit dem Importieren, Exportieren, Erstellen, Anpassen und Entfernen von Erweiterungen können über den Add-on-Manager abgewickelt werden.</string>
<string name="ext__addon_management_box__go_to_page">Gehe zu {ext_home_title}</string>
<string name="ext__home__info">Erweiterungen können über den FlorisBoard Add-on Store heruntergeladen und installiert werden oder importiere beliebige Erweiterungen aus dem Internet.</string>
<string name="ext__home__visit_store">Besuche den Add-ons Store</string>
<string name="ext__home__manage_extensions">Installierte Erweiterungen verwalten</string>
<string name="ext__list__view_details">Details anzeigen</string>
<string name="ext__check_updates__title">Auf Updates prüfen</string>
<!-- Action strings -->
<string name="action__add">Hinzufügen</string>
<string name="action__apply">Übernehmen</string>
@@ -687,6 +756,14 @@
<string name="enum__display_language_names_in__system_locale__description" comment="Enum value description">Sprachnamen in der App und in der Tastatur werden in der Systemsprache des Geräts dargestellt</string>
<string name="enum__display_language_names_in__native_locale" comment="Enum value label">Systemsprache</string>
<string name="enum__display_language_names_in__native_locale__description" comment="Enum value description">Sprachnamen in der App und in der Tastatur werden in der referenzierten Sprache dargestellt</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend" comment="Enum value label">Automatisch Sortieren (Voranstellen)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_prepend__description" comment="Enum value description">Emojis werden bei Verwendung neu sortiert. Neue Emojis werden am Anfang hinzugefügt.</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append" comment="Enum value label">Automatisch Sortieren (Anhängen)</string>
<string name="enum__emoji_history_update_strategy__auto_sort_append__description" comment="Enum value description">Emojis werden bei Verwendung neu sortiert. Neue Emojis werden am Ende hinzugefügt.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend" comment="Enum value label">Manuelles Sortieren (Voranstellen)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_prepend__description" comment="Enum value description">Emojis werden bei Verwendung nicht neu sortiert. Neue Emojis werden am Anfang hinzugefügt.</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append" comment="Enum value label">Manuelles Sortieren (Anhängen)</string>
<string name="enum__emoji_history_update_strategy__manual_sort_append__description" comment="Enum value description">Emojis werden bei Verwendung nicht neu sortiert. Neue Emojis werden am Ende hinzugefügt.</string>
<string name="enum__emoji_skin_tone__default" comment="Enum value label">{emoji} Standard Hautfarbe</string>
<string name="enum__emoji_skin_tone__light_skin_tone" comment="Enum value label">{emoji} Helle Hautfarbe</string>
<string name="enum__emoji_skin_tone__medium_light_skin_tone" comment="Enum value label">{emoji} Mittel helle Hautfarbe</string>
@@ -698,6 +775,10 @@
<string name="enum__emoji_hair_style__curly_hair" comment="Enum value label">{emoji} Lockige Haare</string>
<string name="enum__emoji_hair_style__white_hair" comment="Enum value label">{emoji} Weiße Haare</string>
<string name="enum__emoji_hair_style__bald" comment="Enum value label">{emoji} Glatze</string>
<string name="enum__emoji_suggestion_type__leading_colon">Führender Doppelpunkt</string>
<string name="enum__emoji_suggestion_type__leading_colon__description" comment="Keep the :emoji_name while translating, this is a syntax guide">Emojis mit der Syntax :emoji_name vorschlagen</string>
<string name="enum__emoji_suggestion_type__inline_text">Eingebetteter Text</string>
<string name="enum__emoji_suggestion_type__inline_text__description">Schlage Emojis vor, indem einfach der Emoji-Namen als Wort eingeben wird</string>
<string name="enum__extended_actions_placement__above_candidates" comment="Enum value label">Über Kandidaten</string>
<string name="enum__extended_actions_placement__above_candidates__description" comment="Enum value description">Platziert die erweiterte Aktionsleiste zwischen der App-Oberfläche und der Kandidatenleiste</string>
<string name="enum__extended_actions_placement__below_candidates" comment="Enum value label">Unter Kandidaten</string>
@@ -714,6 +795,8 @@
<string name="enum__key_hint_mode__hint_priority__description" comment="Enum value description">Das erste Zeichen, das nach langem Drücken ausgewählt wird, ist immer das Hinweissymbol, oder der primäre Akzent, wenn kein Hinweissymbol verfügbar ist</string>
<string name="enum__key_hint_mode__smart_priority" comment="Enum value label">Intelligente Priorisierung</string>
<string name="enum__key_hint_mode__smart_priority__description" comment="Enum value description">Das erste Zeichen, das nach langem Drücken ausgewählt wird, wird je nach Sprache und Layout dynamisch, entweder als Hauptakzent oder als Hinweissymbol festgelegt</string>
<string name="enum__incognito_display_mode__replace_shared_actions_toggle" comment="Enum value label">Symbol für \"Geteilte Aktionen\"-Schalter mit dem Inkognito-Indikator ersetzen</string>
<string name="enum__incognito_display_mode__display_behind_keyboard" comment="Enum value label">Inkognito-Indikator hinter die Tastatur anzeigen</string>
<string name="enum__incognito_mode__force_off" comment="Enum value label">Zwingend aus</string>
<string name="enum__incognito_mode__force_off__description" comment="Enum value description">Inkognito-Modus ist immer deaktiviert, egal, was die aktuelle App sagt. Die Inkognito-Aktion in der Smartbar ist nicht verfügbar.</string>
<string name="enum__incognito_mode__force_on" comment="Enum value label">Zwingend an</string>
@@ -811,4 +894,12 @@
<item quantity="one">{v} Element</item>
<item quantity="other">{v} Elemente</item>
</plurals>
<plurals name="unit__characters__written">
<item quantity="one">{v} Zeichen</item>
<item quantity="other">{v} Zeichen</item>
</plurals>
<plurals name="unit__candidates__written">
<item quantity="one">{v} Vorschlag</item>
<item quantity="other">{v} Vorschläge</item>
</plurals>
</resources>

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