Compare commits

...

97 Commits

Author SHA1 Message Date
Patrick Goldinger
bc5ed3475c Release v0.4.6 2025-03-04 22:47:39 +01:00
Patrick Goldinger
871ff0acb1 Merge branch 'main' into release/0.4 2025-03-04 22:45:30 +01:00
florisboard-bot
34f8fec2f6 Update translations from Crowdin 2025-03-04 22:39:56 +01:00
npnpatidar
7199fcdf12 Devanagari Script and Hindi Language Layout Added (#2723)
* test commit

* hi-IN popupmappings id added

* basic layout

* heart replaces with space

* code corrected

* lots of other keys added

* all characters have been added

* popmappings rearranged

* some positional change

* * and \ won't popup due to क्ष and  ज्ञ

* ओ औ code correced

* some vovels reordered

* Hindi does not capitalization concept
2025-03-04 22:27:16 +01:00
Lars Mühlbauer
8319f563b9 Add support for switching the subtype on space bar long press (#2781)
* Add initial support for switching the subtype on space bar long press

* Fix keyboard presses behind the header panel

* Fix taps behind sheet

* Extract string ressource and refactor code

* Instant select subtype

* Persist suptype change in prefs
2025-03-04 22:23:24 +01:00
Lars Mühlbauer
45efe52159 Add new screenshot generator and update fastlane images (#2787)
* Add new screenshot generator and update fastlane images

* Update featureGraphic.png to match g-play requirements
2025-03-04 21:46:54 +01:00
Lars Mühlbauer
7a286b932b Update snygg dynamic color scheme on wallpaper change (#2778)
* Update snygg dynamic color scheme on wallpaper change

* Move broadcast receiver handling to FlorisImeService and properly unregister the receiver

* Set systembar color on stylesheet change
2025-03-04 21:08:45 +01:00
Lars Mühlbauer
df3f84e96d Fix text being rendered behind the top app bar (#2758)
* Fix text being rendered behind the top app bar

This commit fixes an issue where the text was rendered
behind the top app bar in the crash dialog activity by
removing the top app bar in the style and adding a top
app bar in the layout XML.

* Fix toolbar rendering messed up in crash dialog screen

---------

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2025-02-14 13:13:44 +01:00
Lars Mühlbauer
4cc2216940 Fix switching back from voice ime (#2684) (#2759)
This commit fixes an issue where the voice ime was
not possible to switch back to FlorisBoard, because
the keyboard subtype was not specified.
2025-02-14 12:23:46 +01:00
Lars Mühlbauer
21fdd31c88 Fix BottomSheetContent drawing behind system nav bar (#2757)
This commit fixes an issue where the BottomSheetContent
was drawn behind the system navigation bar. To fix this
issue, I applied the saveDrawingPadding Modifier to the
AnimatedVisibility. There was a color problem on the
system navigation bar, so I set the background color to
SheetOutOfBoundsBgColorInactive. Now the system nav bar
is colored the right way.
2025-02-14 12:20:13 +01:00
Lars Mühlbauer
53c6cb52ca Fix crash when opening clipboard history (#2756)
This Commit fixes a crash when opening the clipboard history
with a theme which does not provide a sp size for the header
or the clipboard item. The sp value is now saveTimes multiplied
with 1f.
2025-02-14 12:15:39 +01:00
Patrick Goldinger
9b12213b3e Possible fixes for all keys invisible bug (#2743)
* Fix possible race condition in extension manager init

* Fix deprecation warnings in Kotlin contracts

* Fix race condition in KeyboardManagerResources

* Add debug glide.enabled toggle
2025-02-02 22:35:25 +01:00
Patrick Goldinger
659a5062ab Merge pull request #2750 from florisboard/feat/implement-custom-material-you-color
Implement custom material you colors
2025-02-02 20:17:29 +01:00
lm41
086e5f7782 Add new JetPrefColorPicker for the keyboard accent color.
This commit introduces the new color schemes for spoofed
material you themes. The material you themes can now be
used on any android version that is supported by FlorisBoard
and not just on android 12+.
2025-02-02 19:40:04 +01:00
lm41
ae73e64cd2 Add new JetPrefColorPicker for the settings accent color. 2025-02-02 19:35:16 +01:00
lm41
9f5e8cddd5 Add :lib:color package
This package includes a set of material 3 color schemes
and a map of the corresponding colors to the color schemes.
Please visit the packages README.md for information
on how the schemes were created.
2025-02-02 19:30:39 +01:00
Lars Mühlbauer
3724495d4f Remove code duplication in setup screen (#2742)
* Remove code duplication in setup screen

* fixup! Remove code duplication in setup screen
2025-01-16 22:52:25 +01:00
Patrick Goldinger
a146c6f846 Release v0.4.5 2025-01-13 02:10:22 +01:00
Patrick Goldinger
40ad937384 Merge branch 'main' into release/0.4 2025-01-13 02:08:25 +01:00
Patrick Goldinger
15b5dd9e3e Fix issues with all keys invisible detection (#2737) 2025-01-13 02:07:05 +01:00
Patrick Goldinger
8408cb5133 Release v0.4.4 2025-01-13 00:54:07 +01:00
Patrick Goldinger
8b90cdf06e Merge branch 'main' into release/0.4 2025-01-13 00:44:39 +01:00
florisboard-bot
36f06e0b4b Update translations from Crowdin 2025-01-13 00:34:27 +01:00
Patrick Goldinger
eacb5259ec Merge pull request #2736 from florisboard/chore/add-copyright-templates-to-idea
Update copyright to the new copyright holders
2025-01-13 00:30:04 +01:00
Patrick Goldinger
e7a82a3123 Update README.md/LICENSE 2025-01-13 00:22:59 +01:00
Patrick Goldinger
123b3a6d39 Update copyright year and name in all source files 2025-01-13 00:05:38 +01:00
lm41
9531ad4c36 Add copyright templates for usage with android studio and intellij 2025-01-12 23:56:59 +01:00
Lars Mühlbauer
54ed179ead Fix NoSuchMethodError: removeFirst() on android 14 and lower (#2734)
* Fix NoSuchMethodError: removeFirst() on android 14 and lower

* Add RequiredApi annotations to SnyggMaterialYou classes

---------

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2025-01-12 23:25:20 +01:00
Patrick Goldinger
2dc7efd811 Add better detection for all keys visible bug (#2735)
* Add better detection for all keys visible bug

* Remove on purpose fail for extension manager again
2025-01-12 23:23:38 +01:00
Lars Mühlbauer
01d0b02e6d Fix keyboard height calculation on android 15 to match previous android versions (#2722) 2025-01-11 17:31:51 +01:00
Patrick Goldinger
67c083131a Upgrade to Android 15 (#2733) 2025-01-11 17:17:35 +01:00
Lars Mühlbauer
03cde7296e Add option to delete EmojiHistory from settings screen (#2732)
* Add possibility to delete EmojiHistory from settings screen

* Apply review suggestions

* fixup! Apply review suggestions

* Fix stylistic issues
2025-01-11 16:12:23 +01:00
Lars Mühlbauer
32d26bcf80 Add subtypes and installed extensions to debug logs (#2731)
* Add subtypes and installed extensions to debug logs

* Fix debug log output

* Apply suggestion
2025-01-11 15:59:47 +01:00
Lars Mühlbauer
e61a2c6e82 Fix colors in add subtype screen and enhance subtype presets dialog (#2729) 2025-01-11 15:35:55 +01:00
Md. Rifat Hasan Jihan
ce7a97dce6 added Bengali (bn) to non-capitalization group (#2700) 2024-12-01 22:33:02 +01:00
Md. Rifat Hasan Jihan
9f17a1d36c Replace ZWNJ with ZWJ for Bengali 'র‌্য' in keyboard layout (#2696) 2024-12-01 22:32:27 +01:00
Lars Mühlbauer
a248b4f717 Fix the top message in TypingScreen (#2690) 2024-12-01 22:31:26 +01:00
Lars Mühlbauer
c470b792c1 Fix Keyboard height in one-handed mode (#2668) 2024-10-31 15:10:04 +01:00
Lars Mühlbauer
ff5cd1e7c2 Fix smartbar spacing (#2665) 2024-10-31 15:03:01 +01:00
Lars Mühlbauer
32fee44364 Auto clean sensitive clipboard items (#2659)
* Auto clean sensitive clipboard items

* Fix typo :D

* Apply review suggestions
2024-10-25 16:46:08 +02:00
Patrick Goldinger
97edc33d05 Fix NetworkUtils not detecting hostnames with digits correctly (#2660) 2024-10-25 15:16:51 +02:00
Lars Mühlbauer
a89af25eab Improve ExtensionEditScreen (#2656) 2024-10-22 22:47:20 +02:00
Patrick Goldinger
ff01120925 Release v0.4.3 2024-10-21 23:20:13 +02:00
Patrick Goldinger
74b5c845aa Merge branch 'main' into release/0.4 2024-10-21 23:19:26 +02:00
florisboard-bot
66340249d4 Update translations from Crowdin 2024-10-21 23:17:01 +02:00
Lars Mühlbauer
14147ca1b9 Fix migration in clipboard database (#2641)
* Fix migration in clipboard database

* Update database version

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

* Fix utility key not updating correctly (#2648)
2024-10-21 15:34:15 +02:00
Patrick Goldinger
453fb0253a Fix predictive back by removing generic onBackPressedHandler (#2646) 2024-10-19 15:13:35 +02:00
Lucas Sanginetto
13fc7679a2 Fix incorrect bracket labels in IPA symbols layout (#2644) 2024-10-19 11:26:25 +02:00
Patrick Goldinger
2421d13038 Update ROADMAP.md (#2639) 2024-10-18 00:34:15 +02:00
Patrick Goldinger
ddc4f7f1ba Release v0.4.2 2024-10-17 19:12:13 +02:00
Patrick Goldinger
afea8c721f Merge branch 'main' into release/0.4 2024-10-17 19:10:49 +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
Patrick Goldinger
9cffcea246 Release v0.4.1 2024-10-10 20:53:19 +02:00
Patrick Goldinger
5acf80db0f Merge branch 'main' into release/0.4 2024-10-10 16:03:17 +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
441 changed files with 11070 additions and 2230 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

@@ -15,7 +15,7 @@ jobs:
- name: Checkout
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

1
.gitignore vendored
View File

@@ -40,6 +40,7 @@ captures/
# Intellij
*.iml
.idea/
!/.idea/copyright/
# Keystore files
*.jks

6
.idea/copyright/copyright.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright (C) &amp;#36;originalComment.match(&quot;Copyright \(C\) (\d+)&quot;, 1, &quot;-&quot;, &quot;&amp;#36;today.year&quot;)&amp;#36;today.year The FlorisBoard Contributors&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10;http://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
<option name="myName" value="copyright" />
</copyright>
</component>

15
.idea/copyright/profiles_settings.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<component name="CopyrightManager">
<settings default="copyright">
<module2copyright>
<element module="Project Files" copyright="copyright" />
</module2copyright>
<LanguageOptions name="Shell Script">
<option name="fileTypeOverride" value="1" />
</LanguageOptions>
<LanguageOptions name="XML">
<option name="fileTypeOverride" value="1" />
<option name="prefixLines" value="false" />
</LanguageOptions>
<LanguageOptions name="__TEMPLATE__" />
</settings>
</component>

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Patrick Goldinger
Copyright 2020-2025 The FlorisBoard Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,7 +1,7 @@
<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
@@ -10,10 +10,10 @@ fully respecting your privacy. Currently in early-beta state.
<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>
@@ -21,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>
@@ -34,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>
@@ -42,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>
@@ -54,14 +64,13 @@ 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
> [!IMPORTANT]
> Word suggestions/spell checking are not included in the current releases
@@ -105,7 +114,7 @@ Many thanks to [Nikolay Anzarov](https://www.behance.net/nikolayanzarov) ([@Bloo
## License
```
Copyright 2020-2024 Patrick Goldinger
Copyright 2020-2025 The FlorisBoard Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -119,3 +128,12 @@ 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.
```
Thanks to [The FlorisBoard Contributors](https://github.com/florisboard/florisboard/graphs/contributors) for making this project possible!
<!-- 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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,10 +15,12 @@
*/
import java.io.ByteArrayOutputStream
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
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)
@@ -48,7 +50,6 @@ android {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf(
"-Xallow-result-return-type",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-Xjvm-default=all-compatibility",
)
@@ -99,10 +100,6 @@ android {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}
buildTypes {
named("debug") {
applicationIdSuffix = ".debug"
@@ -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 {
@@ -205,6 +202,7 @@ dependencies {
implementation(libs.patrickgold.jetpref.material.ui)
implementation(project(":lib:android"))
implementation(project(":lib:color"))
implementation(project(":lib:kotlin"))
implementation(project(":lib:native"))
implementation(project(":lib:snygg"))

View File

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

View File

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

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* 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
import androidx.test.ext.junit.runners.AndroidJUnit4

View File

@@ -1,18 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020-2022 Patrick Goldinger
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

View File

@@ -49,6 +49,12 @@
"authors": [ "iamrasel" ],
"direction": "ltr"
},
{
"id": "hindi_in",
"label": "हिंदी",
"authors": [ "npnpatidar" ],
"direction": "ltr"
},
{
"id": "bepo",
"label": "BÉPO",
@@ -392,6 +398,12 @@
"authors": [ "patrickgold" ],
"direction": "ltr"
},
{
"id": "tamil",
"label": "Tamil",
"authors": [ "Clem0908" ],
"direction": "ltr"
},
{
"id": "thai_kedmanee",
"label": "Thai Kedmanee",

View File

@@ -0,0 +1,42 @@
[
[
{ "$": "case_selector", "lower": { "code": 2335, "label": "ट" }, "upper": { "code": 2336, "label": "ठ" } },
{ "$": "case_selector", "lower": { "code": 2337, "label": "ड" }, "upper": { "code": 2338, "label": "ढ" } },
{ "$": "case_selector", "lower": { "code": 2375, "label": "े" }, "upper": { "code": 2376, "label": "ै" } },
{ "$": "case_selector", "lower": { "code": 2352, "label": "र" }, "upper": { "code": 2371, "label": "ृ" } },
{ "$": "case_selector", "lower": { "code": 2340, "label": "त" }, "upper": { "code": 2341, "label": "थ" } },
{ "$": "case_selector", "lower": { "code": 2351, "label": "य" }, "upper": { "code": 2399, "label": "य़" } },
{ "$": "case_selector", "lower": { "code": 2369, "label": "ु" }, "upper": { "code": 2370, "label": "ू" } },
{ "$": "case_selector", "lower": { "code": 2367, "label": "ि" }, "upper": { "code": 2368, "label": "ी" } },
{ "$": "case_selector", "lower": { "code": 2379, "label": "ो" }, "upper": { "code": 2380, "label": "ौ" } },
{ "$": "case_selector", "lower": { "code": 2346, "label": "प" }, "upper": { "code": 2398, "label": "फ़" } }
],
[
{ "$": "case_selector", "lower": { "code": 2366, "label": "ा" }, "upper": { "code": 2309, "label": "अ" } },
{ "$": "case_selector", "lower": { "code": 2360, "label": "स" }, "upper": { "code": 2358, "label": "श" } },
{ "$": "case_selector", "lower": { "code": 2342, "label": "द" }, "upper": { "code": 2343, "label": "ध" } },
{ "$": "case_selector", "lower": { "code": 2347, "label": "फ" }, "upper": { "code": 2364, "label": " ़" } },
{ "$": "case_selector", "lower": { "code": 2327, "label": "ग" }, "upper": { "code": 2328, "label": "घ" } },
{ "$": "case_selector", "lower": { "code": 2361, "label": "ह" }, "upper": { "code": 2307, "label": "" } },
{ "$": "case_selector", "lower": { "code": 2332, "label": "ज" }, "upper": { "code": 2333, "label": "झ" } },
{ "$": "case_selector", "lower": { "code": 2325, "label": "क" }, "upper": { "code": 2326, "label": "ख" } },
{ "$": "case_selector", "lower": { "code": 2354, "label": "ल" }, "upper": { "code": 2355, "label": "ळ" } }
],
[
{
"$": "case_selector",
"lower": { "$": "multi_text_key", "codePoints": [2332, 2381, 2334], "label": "ज्ञ" },
"upper": { "code": 2395, "label": "ज़" }
},
{
"$": "case_selector",
"lower": { "$": "multi_text_key", "codePoints": [2325, 2381, 2359], "label": "क्ष" },
"upper": { "code": 2359, "label": "ष" }
},
{ "$": "case_selector", "lower": { "code": 2330, "label": "च" }, "upper": { "code": 2331, "label": "छ" } },
{ "$": "case_selector", "lower": { "code": 2357, "label": "व" }, "upper": { "code": 2381, "label": "्" } },
{ "$": "case_selector", "lower": { "code": 2348, "label": "ब" }, "upper": { "code": 2349, "label": "भ" } },
{ "$": "case_selector", "lower": { "code": 2344, "label": "न" }, "upper": { "code": 2339, "label": "ण" } },
{ "$": "case_selector", "lower": { "code": 2350, "label": "म" }, "upper": { "code": 2306, "label": "ं" } }
]
]

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

@@ -48,6 +48,7 @@
{ "id": "fi", "authors": [ "patrickgold" ] },
{ "id": "fo", "authors": [ "BinFlush" ] },
{ "id": "fr", "authors": [ "patrickgold" ] },
{ "id": "hi-IN", "authors": [ "npnpatidar" ] },
{ "id": "hr", "authors": [ "hedidnothingwrong" ] },
{ "id": "hu", "authors": [ "zoli111, gabik65" ] },
{ "id": "hy", "authors": [ "PJTSearch" ] },
@@ -695,6 +696,16 @@
"numericAdvanced": "org.florisboard.layouts:bengali"
}
},
{
"languageTag": "hi-IN",
"composer": "org.florisboard.composers:appender",
"currencySet": "org.florisboard.currencysets:indian_rupee",
"popupMapping": "org.florisboard.localization:hi-IN",
"preferred": {
"characters": "org.florisboard.layouts:hindi_in",
"numericRow": "org.florisboard.layouts:devanagari"
}
},
{
"languageTag": "ast",
"composer": "org.florisboard.composers:appender",

View File

@@ -72,7 +72,7 @@
"র": {
"main": { "$": "auto_text_key", "code": 2482, "label": "ল" },
"relevant": [
{ "code": -255, "label": "র্য" }
{ "code": -255, "label": "র্য" }
]
},
"ন": {

View File

@@ -0,0 +1,173 @@
{
"all": {
"क": {
"main": { "$": "auto_text_key", "code": 2392, "label": "क़" },
"relevant": [{ "$": "auto_text_key", "code": 2393, "label": "ख़" }]
},
"ग": {
"main": { "$": "auto_text_key", "code": 2394, "label": "ग़" },
"relevant": [{ "$": "auto_text_key", "code": 2427, "label": "ॻ" }]
},
"च": {
"relevant": [
{ "$": "auto_text_key", "code": 2385, "label": " ॑" },
{ "$": "auto_text_key", "code": 2386, "label": " ॒" }
]
},
"ज": {
"main": { "$": "auto_text_key", "code": 2395, "label": "ज़" },
"relevant": [
{ "$": "auto_text_key", "code": 2428, "label": "ॼ" },
{ "$": "auto_text_key", "code": 2425, "label": "ॹ" }
]
},
"ड": {
"main": { "$": "auto_text_key", "code": 2396, "label": "ड़" },
"relevant": [{ "$": "auto_text_key", "code": 2397, "label": "ढ़" }]
},
"त": {
"main": { "$": "multi_text_key", "codePoints": [2340, 2381, 2352], "label": "त्र" }
},
"द": {
"main": { "$": "auto_text_key", "code": 2396, "label": "ड़" },
"relevant": [
{ "$": "auto_text_key", "code": 2430, "label": "ॾ" },
{ "$": "auto_text_key", "code": 2397, "label": "ढ़" },
{ "$": "auto_text_key", "code": 2424, "label": "ॸ" }
]
},
"न": {
"main": { "$": "auto_text_key", "code": 2329, "label": "ङ" },
"relevant": [
{ "$": "auto_text_key", "code": 2345, "label": "ऩ" },
{ "$": "auto_text_key", "code": 2334, "label": "ञ" }
]
},
"फ": {
"main": { "$": "auto_text_key", "code": 2398, "label": "फ़" }
},
"ब": {
"relevant": [
{ "$": "auto_text_key", "code": 2431, "label": "ॿ" },
{ "$": "auto_text_key", "code": 2365, "label": "ऽ" },
{ "$": "auto_text_key", "code": 2416, "label": "॰" }
]
},
"म": {
"main": { "$": "auto_text_key", "code": 2305, "label": "ँ" },
"relevant": [{ "$": "auto_text_key", "code": 2304, "label": "ऀ" }]
},
"य": {
"main": { "$": "auto_text_key", "code": 2426, "label": "ॺ" }
},
"र": {
"main": { "$": "auto_text_key", "code": 2315, "label": "ऋ" },
"relevant": [
{ "$": "auto_text_key", "code": 2400, "label": "ॠ" },
{ "$": "auto_text_key", "code": 2372, "label": "ॄ" },
{ "$": "auto_text_key", "code": 2353, "label": "ऱ" }
]
},
"ल": {
"relevant": [
{ "$": "auto_text_key", "code": 2402, "label": "ॢ" },
{ "$": "auto_text_key", "code": 2403, "label": "ॣ" },
{ "$": "auto_text_key", "code": 2316, "label": "ऌ" },
{ "$": "auto_text_key", "code": 2401, "label": "ॡ" },
{ "$": "auto_text_key", "code": 2356, "label": "ऴ" }
]
},
"व": {
"relevant": [
{ "$": "auto_text_key", "code": 2387, "label": " ॓" },
{ "$": "auto_text_key", "code": 2388, "label": " ॔" }
]
},
"स": {
"main": { "$": "multi_text_key", "codePoints": [2358, 2381, 2352], "label": "श्र" },
"relevant": [{ "$": "auto_text_key", "code": 2359, "label": "ष" }]
},
"ा": {
"main": { "$": "auto_text_key", "code": 2310, "label": "आ" },
"relevant": [
{ "$": "auto_text_key", "code": 2373, "label": "ॅ" },
{ "$": "auto_text_key", "code": 2418, "label": "ॲ" },
{ "$": "auto_text_key", "code": 2308, "label": "ऄ" }
]
},
"ि": {
"main": { "$": "auto_text_key", "code": 2311, "label": "इ" },
"relevant": [{ "$": "auto_text_key", "code": 2312, "label": "ई" }]
},
"ु": {
"main": { "$": "auto_text_key", "code": 2313, "label": "उ" },
"relevant": [
{ "$": "auto_text_key", "code": 2422, "label": "ॶ" },
{ "$": "auto_text_key", "code": 2423, "label": "ॷ" },
{ "$": "auto_text_key", "code": 2390, "label": " ॖ" },
{ "$": "auto_text_key", "code": 2314, "label": "ऊ" },
{ "$": "auto_text_key", "code": 2391, "label": " ॗ" }
]
},
"े": {
"main": { "$": "auto_text_key", "code": 2319, "label": "ए" },
"relevant": [
{ "$": "auto_text_key", "code": 2317, "label": "ऍ" },
{ "$": "auto_text_key", "code": 2318, "label": "ऎ" },
{ "$": "auto_text_key", "code": 2374, "label": " ॆ" },
{ "$": "auto_text_key", "code": 2320, "label": "ऐ" },
{ "$": "auto_text_key", "code": 2382, "label": " ॎ" },
{ "$": "auto_text_key", "code": 2389, "label": " ॕ" }
]
},
"ो": {
"main": { "$": "auto_text_key", "code": 2323, "label": "ओ" },
"relevant": [
{ "$": "auto_text_key", "code": 2322, "label": "ऒ" },
{ "$": "auto_text_key", "code": 2378, "label": " ॊ" },
{ "$": "auto_text_key", "code": 2383, "label": " ॏ" },
{ "$": "auto_text_key", "code": 2421, "label": "ॵ" },
{ "$": "auto_text_key", "code": 2384, "label": "ॐ" },
{ "$": "auto_text_key", "code": 2377, "label": "ॉ" },
{ "$": "auto_text_key", "code": 2419, "label": "ॳ" },
{ "$": "auto_text_key", "code": 2420, "label": "ॴ" },
{ "$": "auto_text_key", "code": 2362, "label": " ऺ" },
{ "$": "auto_text_key", "code": 2363, "label": " ऻ" },
{ "$": "auto_text_key", "code": 2324, "label": "औ" },
{ "$": "auto_text_key", "code": 2321, "label": "ऑ" }
]
},
"~right": {
"main": { "code": 2404, "label": "।" },
"relevant": [
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 59, "label": ";" },
{ "code": 47, "label": "/" },
{ "$": "layout_direction_selector", "ltr": { "code": 40, "label": "(" }, "rtl": { "code": 41, "label": "(" } },
{ "$": "layout_direction_selector", "ltr": { "code": 41, "label": ")" }, "rtl": { "code": 40, "label": ")" } },
{ "code": 8205, "label": ">⁞<" },
{ "code": 8204, "label": "<⁞>" },
{ "code": 2417, "label": "ॱ" },
{ "code": 2429, "label": "" },
{ "code": 33, "label": "!" },
{ "code": 63, "label": "?" },
{ "code": 2405, "label": "॥" }
]
}
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" },
{ "code": -255, "label": ".in" },
{ "code": -255, "label": ".co.in" }
]
}
}
}

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

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Patrick Goldinger
Copyright 2020-2025 The FlorisBoard Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package dev.patrickgold.florisboard
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.inputmethodservice.ExtractEditText
import android.os.Build
@@ -46,6 +47,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
@@ -78,6 +80,8 @@ import dev.patrickgold.florisboard.app.devtools.DevtoolsOverlay
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.ImeUiMode
import dev.patrickgold.florisboard.ime.clipboard.ClipboardInputLayout
import dev.patrickgold.florisboard.ime.core.SelectSubtypePanel
import dev.patrickgold.florisboard.ime.core.isSubtypeSelectionShowing
import dev.patrickgold.florisboard.ime.editor.EditorRange
import dev.patrickgold.florisboard.ime.editor.FlorisEditorInfo
import dev.patrickgold.florisboard.ime.input.InputFeedbackController
@@ -87,6 +91,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
@@ -97,6 +102,7 @@ import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionsEditorPa
import dev.patrickgold.florisboard.ime.text.TextInputLayout
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.ime.theme.WallpaperChangeReceiver
import dev.patrickgold.florisboard.lib.compose.FlorisButton
import dev.patrickgold.florisboard.lib.compose.ProvideLocalizedResources
import dev.patrickgold.florisboard.lib.compose.SystemUiIme
@@ -105,13 +111,6 @@ import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.devtools.flogInfo
import dev.patrickgold.florisboard.lib.devtools.flogWarning
import dev.patrickgold.florisboard.lib.observeAsTransformingState
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.shape
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import dev.patrickgold.florisboard.lib.util.ViewUtils
import dev.patrickgold.florisboard.lib.util.debugSummarize
import dev.patrickgold.florisboard.lib.util.launchActivity
@@ -123,6 +122,13 @@ import org.florisboard.lib.android.isOrientationPortrait
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemServiceOrNull
import org.florisboard.lib.kotlin.collectLatestIn
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.shape
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import java.lang.ref.WeakReference
/**
@@ -230,10 +236,10 @@ class FlorisImeService : LifecycleInputMethodService() {
val imm = ims.systemServiceOrNull(InputMethodManager::class) ?: return false
val list: List<InputMethodInfo> = imm.enabledInputMethodList
for (el in list) {
for (i in 0 until el.subtypeCount){
for (i in 0 until el.subtypeCount) {
if (el.getSubtypeAt(i).mode != "voice") continue
if (AndroidVersion.ATLEAST_API28_P) {
ims.switchInputMethod(el.id)
ims.switchInputMethod(el.id, el.getSubtypeAt(i))
return true
} else {
ims.window.window?.let { window ->
@@ -265,6 +271,8 @@ class FlorisImeService : LifecycleInputMethodService() {
private var isExtractUiShown by mutableStateOf(false)
private var resourcesContext by mutableStateOf(this as Context)
private val wallpaperChangeReceiver = WallpaperChangeReceiver()
init {
setTheme(R.style.FlorisImeTheme)
}
@@ -278,6 +286,8 @@ class FlorisImeService : LifecycleInputMethodService() {
config.setLocale(subtype.primaryLocale.base)
resourcesContext = createConfigurationContext(config)
}
@Suppress("DEPRECATION") // We do not retrieve the wallpaper but only listen to changes
registerReceiver(wallpaperChangeReceiver, IntentFilter(Intent.ACTION_WALLPAPER_CHANGED))
}
override fun onCreateInputView(): View {
@@ -316,6 +326,7 @@ class FlorisImeService : LifecycleInputMethodService() {
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(wallpaperChangeReceiver)
FlorisImeServiceReference = WeakReference(null)
inputWindowView = null
}
@@ -370,7 +381,7 @@ class FlorisImeService : LifecycleInputMethodService() {
flogInfo { "(no args)" }
super.onFinishInput()
editorInstance.handleFinishInput()
nlpManager.clearInlineSuggestions()
NlpInlineAutofill.clearInlineSuggestions()
}
override fun onWindowShown() {
@@ -437,23 +448,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 +477,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?) {
@@ -491,7 +504,9 @@ class FlorisImeService : LifecycleInputMethodService() {
outInsets.visibleTopInsets = visibleTopY
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
val left = 0
val top = if (keyboardManager.activeState.isBottomSheetShowing()) { 0 } else {
val top = if (keyboardManager.activeState.isBottomSheetShowing() || keyboardManager.activeState.isSubtypeSelectionShowing()) {
0
} else {
visibleTopY - if (needAdditionalOverlay) FlorisImeSizing.Static.smartbarHeightPx else 0
}
val right = inputViewSize.width
@@ -548,7 +563,7 @@ class FlorisImeService : LifecycleInputMethodService() {
.fillMaxWidth()
.weight(1f),
) {
DevtoolsUi()
DevtoolsOverlay(modifier = Modifier.fillMaxSize())
}
}
ImeUi()
@@ -594,6 +609,7 @@ class FlorisImeService : LifecycleInputMethodService() {
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.safeDrawingPadding()
// FIXME: removing this fixes the Smartbar sizing but breaks one-handed-mode
//.height(IntrinsicSize.Min)
.padding(bottom = bottomOffset),
@@ -634,14 +650,6 @@ class FlorisImeService : LifecycleInputMethodService() {
}
}
@Composable
private fun DevtoolsUi() {
val devtoolsEnabled by prefs.devtools.enabled.observeAsState()
if (devtoolsEnabled) {
DevtoolsOverlay(modifier = Modifier.fillMaxSize())
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean =
if (keyboardManager.onHardwareKeyDown(keyCode, event)) true
else super.onKeyDown(keyCode, event)
@@ -683,12 +691,22 @@ class FlorisImeService : LifecycleInputMethodService() {
ProvideLocalizedResources(resourcesContext, forceLayoutDirection = LayoutDirection.Ltr) {
FlorisImeTheme {
BottomSheetHostUi(
isShowing = state.isBottomSheetShowing(),
isShowing = state.isBottomSheetShowing() || state.isSubtypeSelectionShowing(),
onHide = {
keyboardManager.activeState.isActionsEditorVisible = false
if (state.isBottomSheetShowing()) {
keyboardManager.activeState.isActionsEditorVisible = false
}
if (state.isSubtypeSelectionShowing()) {
keyboardManager.activeState.isSubtypeSelectionVisible = false
}
},
) {
QuickActionsEditorPanel()
if (state.isBottomSheetShowing()) {
QuickActionsEditorPanel()
}
if (state.isSubtypeSelectionShowing()) {
SelectSubtypePanel()
}
}
}
}
@@ -737,7 +755,8 @@ class FlorisImeService : LifecycleInputMethodService() {
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
val fieldColor = fieldStyle.foreground.solidColor(context, FlorisImeTheme.fallbackContentColor())
val fieldColor =
fieldStyle.foreground.solidColor(context, FlorisImeTheme.fallbackContentColor())
AndroidView(
modifier = Modifier
.padding(8.dp)
@@ -773,8 +792,14 @@ class FlorisImeService : LifecycleInputMethodService() {
?: "ACTION",
shape = actionStyle.shape.shape(),
colors = ButtonDefaults.buttonColors(
containerColor = actionStyle.background.solidColor(context, FlorisImeTheme.fallbackContentColor()),
contentColor = actionStyle.foreground.solidColor(context, FlorisImeTheme.fallbackSurfaceColor()),
containerColor = actionStyle.background.solidColor(
context,
FlorisImeTheme.fallbackContentColor()
),
contentColor = actionStyle.foreground.solidColor(
context,
FlorisImeTheme.fallbackSurfaceColor()
),
),
)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package dev.patrickgold.florisboard.app
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import dev.patrickgold.florisboard.app.settings.theme.DisplayColorsAs
import dev.patrickgold.florisboard.app.settings.theme.DisplayKbdAfterDialogs
@@ -31,8 +32,9 @@ import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.ime.keyboard.SpaceBarMode
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHairStyle
import dev.patrickgold.florisboard.ime.media.emoji.EmojiRecentlyUsedHelper
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHistory
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSkinTone
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSuggestionType
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
@@ -46,16 +48,21 @@ import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
import org.florisboard.lib.android.isOrientationPortrait
import dev.patrickgold.florisboard.lib.compose.ColorPreferenceSerializer
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsTransformingState
import org.florisboard.lib.snygg.SnyggLevel
import dev.patrickgold.florisboard.lib.util.VersionName
import dev.patrickgold.jetpref.datastore.JetPref
import dev.patrickgold.jetpref.datastore.model.PreferenceMigrationEntry
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
import dev.patrickgold.jetpref.datastore.model.PreferenceType
import dev.patrickgold.jetpref.datastore.model.observeAsState
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.isOrientationPortrait
import org.florisboard.lib.color.DEFAULT_GREEN
import org.florisboard.lib.snygg.SnyggLevel
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
@@ -66,9 +73,13 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "advanced__settings_theme",
default = AppTheme.AUTO,
)
val useMaterialYou = boolean(
key = "advanced__use_material_you",
default = true,
val accentColor = custom(
key = "advanced__accent_color",
default = when (AndroidVersion.ATLEAST_API31_S) {
true -> Color.Unspecified
false -> DEFAULT_GREEN
},
serializer = ColorPreferenceSerializer,
)
val settingsLanguage = string(
key = "advanced__settings_language",
@@ -115,6 +126,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "clipboard__clean_up_after",
default = 20,
)
val autoCleanSensitive = boolean(
key = "clipboard__auto_clean_sensitive",
default = false,
)
val autoCleanSensitiveAfter = int(
key = "clipboard__auto_clean_sensitive_after",
default = 20,
)
val limitHistorySize = boolean(
key = "clipboard__limit_history_size",
default = true,
@@ -155,10 +174,6 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "devtools__enabled",
default = false,
)
val showHeapMemoryStats = boolean(
key = "devtools__show_heap_memory_stats",
default = false,
)
val showPrimaryClip = boolean(
key = "devtools__show_primary_clip",
default = false,
@@ -171,6 +186,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 +212,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 +610,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 +633,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,
@@ -652,6 +712,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
default = extCoreTheme("floris_night"),
serializer = ExtensionComponentName.Serializer,
)
val accentColor = custom(
key = "theme__accent_color",
default = when (AndroidVersion.ATLEAST_API31_S) {
true -> Color.Unspecified
false -> DEFAULT_GREEN
},
serializer = ColorPreferenceSerializer,
)
//val sunriseTime = localTime(
// key = "theme__sunrise_time",
// default = LocalTime.of(6, 0),
@@ -683,8 +751,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 +773,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

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app
import androidx.compose.runtime.Composable
@@ -11,7 +27,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 +156,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 +226,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(
@@ -486,6 +542,10 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
key = SwipeAction.SHOW_INPUT_METHOD_PICKER,
label = stringRes(R.string.enum__swipe_action__show_input_method_picker),
)
entry(
key = SwipeAction.SHOW_SUBTYPE_PICKER,
label = "Show subtype picker"
)
entry(
key = SwipeAction.SWITCH_TO_PREV_SUBTYPE,
label = stringRes(R.string.enum__swipe_action__switch_to_prev_subtype),

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,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.graphics.isUnspecified
import androidx.compose.ui.platform.LocalConfiguration
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
@@ -118,8 +118,8 @@ class FlorisAppActivity : ComponentActivity() {
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
setContent {
ProvideLocalizedResources(resourcesContext) {
val useMaterialYou by prefs.advanced.useMaterialYou.observeAsState()
FlorisAppTheme(theme = appTheme, isMaterialYouAware = useMaterialYou) {
val accentColor by prefs.advanced.accentColor.observeAsState()
FlorisAppTheme(theme = appTheme, isMaterialYouAware = accentColor.isUnspecified) {
Surface(color = MaterialTheme.colorScheme.background) {
AppContent()
}
@@ -144,19 +144,19 @@ 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.categories?.contains(Intent.CATEGORY_BROWSABLE) == true) {
if (intent.action == Intent.ACTION_VIEW && intent.categories?.contains(Intent.CATEGORY_BROWSABLE) == true) {
intentToBeHandled = intent
return
}
if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
intentToBeHandled = intent
return
}
if (intent?.action == Intent.ACTION_SEND && intent.clipData != null) {
if (intent.action == Intent.ACTION_SEND && intent.clipData != null) {
intentToBeHandled = intent
return
}
@@ -215,9 +215,5 @@ class FlorisAppActivity : ComponentActivity() {
}
intentToBeHandled = null
}
SideEffect {
navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher)
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,19 +17,24 @@
package dev.patrickgold.florisboard.app.apptheme
import android.app.Activity
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
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.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import dev.patrickgold.florisboard.app.AppTheme
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.color.ColorMappings
/*private val AmoledDarkColorPalette = darkColorScheme(
primary = Green500,
@@ -70,170 +75,93 @@ private val LightColorPalette = lightColorScheme(
*/
)*/
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme = darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
@Composable
fun getColorScheme(
context: Context,
isMaterialYouAware: Boolean,
themeColor: Color,
theme: AppTheme,
): ColorScheme {
val isDark = isSystemInDarkTheme()
private val amoledScheme = darkScheme.copy(
background = amoledDark,
surface = amoledDark
)
return when (theme) {
AppTheme.AUTO -> {
if (isMaterialYouAware && AndroidVersion.ATLEAST_API31_S) {
when {
isDark -> dynamicDarkColorScheme(context)
else -> dynamicLightColorScheme(context)
}
} else {
ColorMappings.getColorSchemeOrDefault(themeColor, isDark, true)
}
}
AppTheme.DARK -> {
if (isMaterialYouAware && AndroidVersion.ATLEAST_API31_S) {
dynamicDarkColorScheme(context)
} else {
ColorMappings.getColorSchemeOrDefault(themeColor, isDark = true, settings = true)
}
}
AppTheme.LIGHT -> {
if (isMaterialYouAware && AndroidVersion.ATLEAST_API31_S) {
dynamicLightColorScheme(context)
} else {
ColorMappings.getColorSchemeOrDefault(themeColor, isDark = false, settings = true)
}
}
AppTheme.AMOLED_DARK -> {
if (isMaterialYouAware && AndroidVersion.ATLEAST_API31_S) {
dynamicDarkColorScheme(context).amoled()
} else {
ColorMappings.getColorSchemeOrDefault(themeColor, isDark = true, settings = true).amoled()
}
}
AppTheme.AUTO_AMOLED -> {
if (isMaterialYouAware && AndroidVersion.ATLEAST_API31_S) {
when {
isDark -> dynamicDarkColorScheme(context).amoled()
else -> dynamicLightColorScheme(context)
}
} else {
with(ColorMappings.getColorSchemeOrDefault(themeColor, isDark, true)) {
if (isDark) amoled() else this
}
}
}
}
}
fun ColorScheme.amoled(): ColorScheme {
return this.copy(background = Color.Black, surface = Color.Black)
}
@Composable
fun FlorisAppTheme(
theme: AppTheme,
isMaterialYouAware: Boolean,
content: @Composable () -> Unit
content: @Composable () -> Unit,
) {
val prefs by florisPreferenceModel()
val accent by prefs.advanced.accentColor.observeAsState()
val colors = if (AndroidVersion.ATLEAST_API31_S) {
when (theme) {
AppTheme.AUTO -> when {
isMaterialYouAware -> when {
isSystemInDarkTheme() -> dynamicDarkColorScheme(LocalContext.current)
else -> dynamicLightColorScheme(LocalContext.current)
}
else -> {
when {
isSystemInDarkTheme() -> darkScheme
else -> lightScheme
}
}
}
AppTheme.AUTO_AMOLED -> when {
isMaterialYouAware -> when {
isSystemInDarkTheme() -> dynamicDarkColorScheme(LocalContext.current).copy(
background = amoledDark,
surface = amoledDark,
)
else -> dynamicLightColorScheme(LocalContext.current)
}
else -> {
when {
isSystemInDarkTheme() -> amoledScheme
else -> lightScheme
}
}
}
AppTheme.LIGHT -> when {
isMaterialYouAware -> dynamicLightColorScheme(LocalContext.current)
else -> lightScheme
}
AppTheme.DARK -> when {
isMaterialYouAware -> dynamicDarkColorScheme(LocalContext.current)
else -> darkScheme
}
AppTheme.AMOLED_DARK -> when {
isMaterialYouAware -> dynamicDarkColorScheme(LocalContext.current).copy(
background = amoledDark,
surface = amoledDark,
)
else -> amoledScheme
}
}
} else {
when (theme) {
AppTheme.AUTO -> when {
isSystemInDarkTheme() -> darkScheme
else -> lightScheme
}
AppTheme.AUTO_AMOLED -> when {
isSystemInDarkTheme() -> darkScheme
else -> lightScheme
}
AppTheme.LIGHT -> lightScheme
AppTheme.DARK -> darkScheme
AppTheme.AMOLED_DARK -> amoledScheme
}
}
val colors = getColorScheme(
context = LocalContext.current,
theme = theme,
isMaterialYouAware = isMaterialYouAware,
themeColor = accent,
)
val darkTheme =
theme == AppTheme.DARK
|| theme == AppTheme.AMOLED_DARK
|| (theme == AppTheme.AUTO && isSystemInDarkTheme())
|| (theme == AppTheme.AUTO_AMOLED && isSystemInDarkTheme())
|| theme == AppTheme.AMOLED_DARK
|| (theme == AppTheme.AUTO && isSystemInDarkTheme())
|| (theme == AppTheme.AUTO_AMOLED && isSystemInDarkTheme())
val view = LocalView.current
if (!view.isInEditMode) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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,15 @@ 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.keyboard.CachedLayout
import dev.patrickgold.florisboard.ime.keyboard.DebugLayoutComputationResult
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofill
import dev.patrickgold.florisboard.keyboardManager
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.*
@@ -52,26 +59,38 @@ private val DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", FlorisLocale.
@Composable
fun DevtoolsOverlay(modifier: Modifier = Modifier) {
val context = LocalContext.current
val prefs by florisPreferenceModel()
val keyboardManager by context.keyboardManager()
val devtoolsEnabled by prefs.devtools.enabled.observeAsState()
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()
val debugLayoutResult by keyboardManager.layoutManager.debugLayoutComputationResultFlow.collectAsState()
CompositionLocalProvider(
LocalContentColor provides Color.White,
LocalLayoutDirection provides LayoutDirection.Ltr,
) {
Column(modifier = modifier) {
if (showPrimaryClip) {
if (devtoolsEnabled && showPrimaryClip) {
DevtoolsClipboardOverlay()
}
if (showInputStateOverlay) {
if (devtoolsEnabled && showInputStateOverlay) {
DevtoolsInputStateOverlay()
}
if (showSpellingOverlay) {
if (debugLayoutResult?.allLayoutsSuccess() == false) {
DevtoolsLastLayoutComputationOverlay(debugLayoutResult)
}
if (devtoolsEnabled && showSpellingOverlay) {
DevtoolsSpellingOverlay()
}
if (devtoolsEnabled && showInlineAutofillOverlay && AndroidVersion.ATLEAST_API30_R) {
DevtoolsInlineAutofillOverlay()
}
}
}
}
@@ -117,6 +136,33 @@ private fun DevtoolsInputStateOverlay() {
}
}
@Composable
private fun DevtoolsLastLayoutComputationOverlay(debugLayoutResult: DebugLayoutComputationResult?) {
@Composable
fun PrintResult(result: Result<CachedLayout?>) {
if (result.isSuccess) {
DevtoolsText(text = "loaded: ${result.getOrNull()?.name}")
} else {
DevtoolsText(text = "error: ${result.exceptionOrNull()}")
}
}
DevtoolsOverlayBox(title = "Last layout computation") {
if (debugLayoutResult == null) {
DevtoolsText(text = "No layout computation result available.")
return@DevtoolsOverlayBox
}
DevtoolsSubGroup(title = "main") {
PrintResult(debugLayoutResult!!.main)
}
DevtoolsSubGroup(title = "mod") {
PrintResult(debugLayoutResult!!.mod)
}
DevtoolsSubGroup(title = "ext") {
PrintResult(debugLayoutResult!!.ext)
}
}
}
@Composable
private fun DevtoolsSpellingOverlay() {
@@ -160,6 +206,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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 :)"
@@ -60,12 +61,6 @@ fun DevtoolsScreen() = FlorisScreen {
)
PreferenceGroup(title = stringRes(R.string.devtools__title)) {
SwitchPreference(
prefs.devtools.showHeapMemoryStats,
title = stringRes(R.string.devtools__show_heap_memory_stats__label),
summary = stringRes(R.string.devtools__show_heap_memory_stats__summary),
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
SwitchPreference(
prefs.devtools.showPrimaryClip,
title = stringRes(R.string.devtools__show_primary_clip__label),
@@ -84,6 +79,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),
@@ -120,6 +122,13 @@ fun DevtoolsScreen() = FlorisScreen {
onClick = { navController.navigate(Routes.Devtools.ExportDebugLog) },
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
SwitchPreference(
prefs.glide.enabled,
title = "prefs.glide.enabled (debug)",
summaryOn = "This impacts your performance and may trigger the all keys invisible bug!",
summaryOff = "Recommended to keep this off!",
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
}
PreferenceGroup(title = stringRes(R.string.devtools__group_android__title)) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ext
import androidx.compose.foundation.layout.Row

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ext
import androidx.compose.runtime.Composable

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ext
import androidx.compose.foundation.layout.Row

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,13 +19,15 @@ package dev.patrickgold.florisboard.app.settings.advanced
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Adb
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.FormatPaint
import androidx.compose.material.icons.filled.FormatColorFill
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Preview
import androidx.compose.material.icons.filled.SettingsBackupRestore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.AppTheme
import dev.patrickgold.florisboard.app.LocalNavController
@@ -34,16 +36,20 @@ import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.lib.FlorisLocale
import org.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ColorPickerPreference
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.datastore.ui.SwitchPreference
import dev.patrickgold.jetpref.datastore.ui.isMaterialYou
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.color.ColorMappings
@Composable
fun AdvancedScreen() = FlorisScreen {
@@ -51,6 +57,7 @@ fun AdvancedScreen() = FlorisScreen {
previewFieldVisible = false
val navController = LocalNavController.current
val context = LocalContext.current
content {
ListPreference(
@@ -59,13 +66,21 @@ fun AdvancedScreen() = FlorisScreen {
title = stringRes(R.string.pref__advanced__settings_theme__label),
entries = enumDisplayEntriesOf(AppTheme::class),
)
SwitchPreference(
pref = prefs.advanced.useMaterialYou,
icon = Icons.Default.FormatPaint,
title = stringRes(R.string.pref__advanced__settings_material_you__label),
visibleIf = {
AndroidVersion.ATLEAST_API31_S
},
ColorPickerPreference(
pref = prefs.advanced.accentColor,
title = stringRes(R.string.pref__advanced__settings_accent_color__label),
defaultValueLabel = stringRes(R.string.action__default),
icon = Icons.Default.FormatColorFill,
defaultColors = ColorMappings.colors,
showAlphaSlider = false,
enableAdvancedLayout = false,
colorOverride = {
if (it.isMaterialYou(context)) {
Color.Unspecified
} else {
it
}
}
)
ListPreference(
prefs.advanced.settingsLanguage,

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import org.florisboard.lib.android.AndroidVersion
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
@@ -71,6 +72,22 @@ fun ClipboardScreen() = FlorisScreen {
stepIncrement = 5,
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.cleanUpOld isEqualTo true },
)
SwitchPreference(
prefs.clipboard.autoCleanSensitive,
title = stringRes(R.string.pref__clipboard__auto_clean_sensitive__label),
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true },
visibleIf = { AndroidVersion.ATLEAST_API33_T },
)
DialogSliderPreference(
prefs.clipboard.autoCleanSensitiveAfter,
title = stringRes(R.string.pref__clipboard__auto_clean_sensitive_after__label),
valueLabel = { pluralsRes(R.plurals.unit__seconds__written, it, "v" to it) },
min = 0,
max = 300,
stepIncrement = 10,
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.autoCleanSensitive isEqualTo true },
visibleIf = { AndroidVersion.ATLEAST_API33_T },
)
SwitchPreference(
prefs.clipboard.limitHistorySize,
title = stringRes(R.string.pref__clipboard__limit_history_size__label),

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,12 +25,17 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -47,11 +52,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
@@ -73,6 +78,7 @@ import dev.patrickgold.florisboard.lib.compose.FlorisButtonBar
import dev.patrickgold.florisboard.lib.compose.FlorisDropdownLikeButton
import dev.patrickgold.florisboard.lib.compose.FlorisDropdownMenu
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.florisScrollbar
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsNonNullState
@@ -178,7 +184,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
})
val selectValue = stringRes(R.string.settings__localization__subtype_select_placeholder)
val selectListValues = remember (selectValue) { listOf(selectValue) }
val selectListValues = remember(selectValue) { listOf(selectValue) }
val prefs by florisPreferenceModel()
val navController = LocalNavController.current
@@ -229,7 +235,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
@Composable
fun SubtypePropertyDropdown(
title: String,
layoutType: LayoutType
layoutType: LayoutType,
) {
SubtypeProperty(title) {
SubtypeLayoutDropdown(
@@ -327,6 +333,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
DisplayLanguageNamesIn.NATIVE_LOCALE -> suggestedPreset.locale.displayName(suggestedPreset.locale)
},
secondaryText = suggestedPreset.preferred.characters.componentId,
colors = ListItemDefaults.colors(containerColor = CardDefaults.cardColors().containerColor),
)
}
} else {
@@ -481,20 +488,30 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
showSubtypePresetsDialog = false
},
) {
LazyColumn {
items(subtypePresets) { subtypePreset ->
JetPrefListItem(
modifier = Modifier.clickable {
subtypeEditor.applySubtype(subtypePreset.toSubtype())
showSubtypePresetsDialog = false
},
text = when (displayLanguageNamesIn) {
DisplayLanguageNamesIn.SYSTEM_LOCALE -> subtypePreset.locale.displayName()
DisplayLanguageNamesIn.NATIVE_LOCALE -> subtypePreset.locale.displayName(subtypePreset.locale)
},
secondaryText = subtypePreset.preferred.characters.componentId,
)
Column {
HorizontalDivider()
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.florisScrollbar(lazyListState, isVertical = true).weight(1f),
state = lazyListState,
) {
items(subtypePresets) { subtypePreset ->
JetPrefListItem(
modifier = Modifier.clickable {
subtypeEditor.applySubtype(subtypePreset.toSubtype())
showSubtypePresetsDialog = false
},
text = when (displayLanguageNamesIn) {
DisplayLanguageNamesIn.SYSTEM_LOCALE -> subtypePreset.locale.displayName()
DisplayLanguageNamesIn.NATIVE_LOCALE -> subtypePreset.locale.displayName(subtypePreset.locale)
},
secondaryText = subtypePreset.preferred.characters.componentId,
colors = ListItemDefaults.colors(containerColor = AlertDialogDefaults.containerColor),
)
}
}
HorizontalDivider()
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2024-2025 The FlorisBoard Contributors
*
* 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,202 @@
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHistory
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHistoryHelper
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.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import kotlinx.coroutines.launch
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
fun MediaScreen() = FlorisScreen {
title = stringRes(R.string.settings__media__title)
previewFieldVisible = true
iconSpaceReserved = false
iconSpaceReserved = true
val prefs by florisPreferenceModel()
var shouldDelete by remember { mutableStateOf<ShouldDelete?>(null) }
val scope = rememberCoroutineScope()
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)
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() },
)
Preference(
title = stringRes(R.string.prefs__media__emoji_history_pinned_reset),
onClick = {
shouldDelete = ShouldDelete(true)
},
enabledIf = { prefs.emoji.historyEnabled.isTrue() },
)
Preference(
title = stringRes(R.string.prefs__media__emoji_history_reset),
onClick = {
shouldDelete = ShouldDelete(false)
},
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() },
)
}
}
DeleteEmojiHistoryConfirmDialog(
shouldDelete = shouldDelete,
onDismiss = {
shouldDelete = null
},
onConfirm = {
shouldDelete?.let {
scope.launch {
if (it.pinned) {
EmojiHistoryHelper.deletePinned(prefs = prefs)
} else {
EmojiHistoryHelper.deleteHistory(prefs = prefs)
}
}
},
min = 0,
max = 120,
stepIncrement = 1,
)
shouldDelete = null
}
},
)
}
@Composable
fun DeleteEmojiHistoryConfirmDialog(
shouldDelete: ShouldDelete?,
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
shouldDelete?.let {
JetPrefAlertDialog(
title = stringRes(R.string.action__reset_confirm_title),
confirmLabel = stringRes(R.string.action__yes),
dismissLabel = stringRes(R.string.action__no),
onDismiss = onDismiss,
onConfirm = onConfirm,
) {
if (it.pinned) {
Text(stringRes(R.string.action__reset_confirm_message, "name" to "pinned emojis"))
} else {
Text(stringRes(R.string.action__reset_confirm_message, "name" to "emoji history"))
}
}
}
}
data class ShouldDelete(val pinned: Boolean)

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,12 +19,14 @@ package dev.patrickgold.florisboard.app.settings.theme
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrightnessAuto
import androidx.compose.material.icons.filled.ColorLens
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
@@ -41,8 +43,11 @@ import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ColorPickerPreference
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.isMaterialYou
import org.florisboard.lib.color.ColorMappings
@Composable
fun ThemeScreen() = FlorisScreen {
@@ -108,6 +113,22 @@ fun ThemeScreen() = FlorisScreen {
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_NIGHT))
},
)
ColorPickerPreference(
pref = prefs.theme.accentColor,
title = stringRes(R.string.pref__theme__theme_accent_color__label),
defaultValueLabel = stringRes(R.string.action__default),
icon = Icons.Default.ColorLens,
defaultColors = ColorMappings.colors,
showAlphaSlider = false,
enableAdvancedLayout = false,
colorOverride = {
if (it.isMaterialYou(context)) {
Color.Unspecified
} else {
it
}
}
)
AddonManagementReferenceBox(type = ExtensionListScreenType.EXT_THEME)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,7 +36,6 @@ import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
import org.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
import dev.patrickgold.florisboard.lib.compose.FlorisHyperlinkText
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
@@ -48,6 +47,7 @@ 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.datastore.ui.SwitchPreference
import org.florisboard.lib.android.AndroidVersion
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
@@ -58,11 +58,11 @@ fun TypingScreen() = FlorisScreen {
val navController = LocalNavController.current
content {
// This card is temporary and is therefore not using a string resource
// This card is temporary and is therefore not using a string resource (not so temporary as we thought...)
FlorisErrorCard(
modifier = Modifier.padding(8.dp),
text = """
Suggestions (except system autofill) and spell checking are not available in this alpha release. All
Suggestions (except system autofill) and spell checking are not available in this release. All
preferences in the "Corrections" group are properly implemented though.
""".trimIndent().replace('\n', ' '),
)

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.setup
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,9 +45,6 @@ import dev.patrickgold.florisboard.app.FlorisAppActivity
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.florisPreferenceModel
import org.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.util.launchActivity
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.florisboard.lib.compose.FlorisBulletSpacer
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisScreenScope
@@ -56,9 +53,12 @@ import dev.patrickgold.florisboard.lib.compose.FlorisStepLayout
import dev.patrickgold.florisboard.lib.compose.FlorisStepState
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.util.InputMethodUtils
import dev.patrickgold.florisboard.lib.util.launchActivity
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.PreferenceUiScope
import kotlinx.coroutines.delay
import org.florisboard.lib.android.AndroidVersion
@Composable
@@ -105,128 +105,65 @@ private fun FlorisScreenScope.content(
hasNotificationPermission: NotificationPermissionState,
) {
// Show screen without notification permission if the android version is below android 13.
if (AndroidVersion.ATMOST_API32_S_V2) {
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
val initStep = when {
!isFlorisBoardEnabled -> Steps.EnableIme.id
!isFlorisBoardSelected -> Steps.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET && AndroidVersion.ATLEAST_API33_T -> Steps.SelectNotification.id
else -> Steps.FinishUp.id
}
FlorisStepState.new(init = initStep)
}
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
val initStep = when {
!isFlorisBoardEnabled -> Steps.WithoutNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithoutNotifications.SelectIme.id
else -> Steps.WithoutNotifications.FinishUp.id
}
FlorisStepState.new(init = initStep)
content {
LaunchedEffect(isFlorisBoardEnabled, isFlorisBoardSelected, hasNotificationPermission) {
stepState.setCurrentAuto(
when {
!isFlorisBoardEnabled -> Steps.EnableIme.id
!isFlorisBoardSelected -> Steps.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET && AndroidVersion.ATLEAST_API33_T -> Steps.SelectNotification.id
else -> Steps.FinishUp.id
}
)
}
content {
LaunchedEffect(isFlorisBoardEnabled, isFlorisBoardSelected) {
stepState.setCurrentAuto(
when {
!isFlorisBoardEnabled -> Steps.WithoutNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithoutNotifications.SelectIme.id
else -> Steps.WithoutNotifications.FinishUp.id
}
)
}
// Below block allows to return from the system IME enabler activity
// as soon as it gets selected.
LaunchedEffect(Unit) {
while (true) {
delay(200L)
val isEnabled = InputMethodUtils.isFlorisboardEnabled(context)
if (stepState.getCurrentAuto().value == Steps.WithoutNotifications.EnableIme.id &&
stepState.getCurrentManual().value == -1 &&
!isFlorisBoardEnabled &&
!isFlorisBoardSelected &&
isEnabled
) {
context.launchActivity(FlorisAppActivity::class) {
it.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
// Below block allows to return from the system IME enabler activity
// as soon as it gets selected.
LaunchedEffect(Unit) {
while (true) {
delay(200L)
val isEnabled = InputMethodUtils.isFlorisboardEnabled(context)
if (stepState.getCurrentAuto().value == Steps.EnableIme.id &&
stepState.getCurrentManual().value == -1 &&
!isFlorisBoardEnabled &&
!isFlorisBoardSelected &&
hasNotificationPermission == NotificationPermissionState.NOT_SET &&
isEnabled
) {
context.launchActivity(FlorisAppActivity::class) {
it.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
FlorisStepLayout(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
stepState = stepState,
header = {
StepText(stringRes(R.string.setup__intro_message))
Spacer(modifier = Modifier.height(16.dp))
},
steps = steps(
context, navController, requestNotification
),
footer = {
footer(context)
},
)
}
// Show the screen with notification permission on android 13+
} else {
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
val initStep = when {
!isFlorisBoardEnabled -> Steps.WithNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithNotifications.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET -> Steps.WithNotifications.SelectNotification.id
else -> Steps.WithNotifications.FinishUp.id
}
FlorisStepState.new(init = initStep)
}
content {
LaunchedEffect(isFlorisBoardEnabled, isFlorisBoardSelected, hasNotificationPermission) {
stepState.setCurrentAuto(
when {
!isFlorisBoardEnabled -> Steps.WithNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithNotifications.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET -> Steps.WithNotifications.SelectNotification.id
else -> Steps.WithNotifications.FinishUp.id
}
)
}
// Below block allows to return from the system IME enabler activity
// as soon as it gets selected.
LaunchedEffect(Unit) {
while (true) {
delay(200L)
val isEnabled = InputMethodUtils.isFlorisboardEnabled(context)
if (stepState.getCurrentAuto().value == Steps.WithNotifications.EnableIme.id &&
stepState.getCurrentManual().value == -1 &&
!isFlorisBoardEnabled &&
!isFlorisBoardSelected &&
hasNotificationPermission == NotificationPermissionState.NOT_SET &&
isEnabled
) {
context.launchActivity(FlorisAppActivity::class) {
it.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
}
FlorisStepLayout(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
stepState = stepState,
header = {
StepText(stringRes(R.string.setup__intro_message))
Spacer(modifier = Modifier.height(16.dp))
},
steps = steps(
context, navController, requestNotification
),
footer = {
footer(context)
},
)
}
FlorisStepLayout(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
stepState = stepState,
header = {
StepText(stringRes(R.string.setup__intro_message))
Spacer(modifier = Modifier.height(16.dp))
},
steps = steps(
context, navController, requestNotification
),
footer = {
footer(context)
},
)
}
}
@@ -255,105 +192,60 @@ private fun footer(context: Context) {
private fun PreferenceUiScope<AppPrefs>.steps(
context: Context,
navController: NavController,
requestNotification: ManagedActivityResultLauncher<String, Boolean>
requestNotification: ManagedActivityResultLauncher<String, Boolean>,
): List<FlorisStep> {
return if (AndroidVersion.ATMOST_API32_S_V2) {
listOf(
return listOfNotNull(
FlorisStep(
id = Steps.EnableIme.id,
title = stringRes(R.string.setup__enable_ime__title),
) {
StepText(stringRes(R.string.setup__enable_ime__description))
StepButton(label = stringRes(R.string.setup__enable_ime__open_settings_btn)) {
InputMethodUtils.showImeEnablerActivity(context)
}
},
FlorisStep(
id = Steps.SelectIme.id,
title = stringRes(R.string.setup__select_ime__title),
) {
StepText(stringRes(R.string.setup__select_ime__description))
StepButton(label = stringRes(R.string.setup__select_ime__switch_keyboard_btn)) {
InputMethodUtils.showImePicker(context)
}
},
if (AndroidVersion.ATLEAST_API33_T) {
FlorisStep(
id = Steps.WithoutNotifications.EnableIme.id,
title = stringRes(R.string.setup__enable_ime__title),
) {
StepText(stringRes(R.string.setup__enable_ime__description))
StepButton(label = stringRes(R.string.setup__enable_ime__open_settings_btn)) {
InputMethodUtils.showImeEnablerActivity(context)
}
},
FlorisStep(
id = Steps.WithoutNotifications.SelectIme.id,
title = stringRes(R.string.setup__select_ime__title),
) {
StepText(stringRes(R.string.setup__select_ime__description))
StepButton(label = stringRes(R.string.setup__select_ime__switch_keyboard_btn)) {
InputMethodUtils.showImePicker(context)
}
},
FlorisStep(
id = Steps.WithoutNotifications.FinishUp.id,
title = stringRes(R.string.setup__finish_up__title),
) {
StepText(stringRes(R.string.setup__finish_up__description_p1))
StepText(stringRes(R.string.setup__finish_up__description_p2))
StepButton(label = stringRes(R.string.setup__finish_up__finish_btn)) {
this@steps.prefs.internal.isImeSetUp.set(true)
navController.navigate(Routes.Settings.Home) {
popUpTo(Routes.Setup.Screen) {
inclusive = true
}
}
}
},
)
} else {
listOf(
FlorisStep(
id = Steps.WithNotifications.EnableIme.id,
title = stringRes(R.string.setup__enable_ime__title),
) {
StepText(stringRes(R.string.setup__enable_ime__description))
StepButton(label = stringRes(R.string.setup__enable_ime__open_settings_btn)) {
InputMethodUtils.showImeEnablerActivity(context)
}
},
FlorisStep(
id = Steps.WithNotifications.SelectIme.id,
title = stringRes(R.string.setup__select_ime__title),
) {
StepText(stringRes(R.string.setup__select_ime__description))
StepButton(label = stringRes(R.string.setup__select_ime__switch_keyboard_btn)) {
InputMethodUtils.showImePicker(context)
}
},
FlorisStep(
id = Steps.WithNotifications.SelectNotification.id,
title = stringRes(R.string.setup__grant_notification_permission__title)
id = Steps.SelectNotification.id,
title = stringRes(R.string.setup__grant_notification_permission__title),
) {
StepText(stringRes(R.string.setup__grant_notification_permission__description))
StepButton(stringRes(R.string.setup__grant_notification_permission__btn)) {
if (AndroidVersion.ATLEAST_API33_T) {
requestNotification.launch(android.Manifest.permission.POST_NOTIFICATIONS)
requestNotification.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
} else null,
FlorisStep(
id = Steps.FinishUp.id,
title = stringRes(R.string.setup__finish_up__title),
) {
StepText(stringRes(R.string.setup__finish_up__description_p1))
StepText(stringRes(R.string.setup__finish_up__description_p2))
StepButton(label = stringRes(R.string.setup__finish_up__finish_btn)) {
this@steps.prefs.internal.isImeSetUp.set(true)
navController.navigate(Routes.Settings.Home) {
popUpTo(Routes.Setup.Screen) {
inclusive = true
}
}
},
FlorisStep(
id = Steps.WithNotifications.FinishUp.id,
title = stringRes(R.string.setup__finish_up__title),
) {
StepText(stringRes(R.string.setup__finish_up__description_p1))
StepText(stringRes(R.string.setup__finish_up__description_p2))
StepButton(label = stringRes(R.string.setup__finish_up__finish_btn)) {
this@steps.prefs.internal.isImeSetUp.set(true)
navController.navigate(Routes.Settings.Home) {
popUpTo(Routes.Setup.Screen) {
inclusive = true
}
}
}
},
)
}
}
}
)
}
private sealed class Steps(val id: Int) {
sealed class WithoutNotifications(id: Int) : Steps(id) {
data object EnableIme : WithoutNotifications(id = 1)
data object SelectIme : WithoutNotifications(id = 2)
data object FinishUp : WithoutNotifications(id = 3)
}
sealed class WithNotifications(id: Int) : Steps(id) {
data object EnableIme : WithNotifications(id = 1)
data object SelectIme : WithNotifications(id = 2)
data object SelectNotification : WithNotifications(id = 3)
data object FinishUp : WithNotifications(id = 4)
}
data object EnableIme : Steps(id = 1)
data object SelectIme : Steps(id = 2)
data object SelectNotification : Steps(id = 3)
data object FinishUp : Steps(id = 4)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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)
}
}
}
@@ -204,6 +214,7 @@ fun ClipboardInputLayout(
contentScrollInsteadOfClip: Boolean,
modifier: Modifier = Modifier,
) {
val fontSize = style.fontSize.spSize() safeTimes 1f
SnyggSurface(
modifier = modifier
.fillMaxWidth()
@@ -212,7 +223,7 @@ fun ClipboardInputLayout(
clip = true,
clickAndSemanticsModifier = Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
enabled = popupItem == null,
onLongClick = {
popupItem = item
@@ -248,7 +259,7 @@ fun ClipboardInputLayout(
text = bitmap.exceptionOrNull()?.message ?: "Unknown error",
style = TextStyle(textDirection = TextDirection.Ltr),
color = Color.Red,
fontSize = style.fontSize.spSize(),
fontSize = fontSize,
)
}
} else if (item.type == ItemType.VIDEO) {
@@ -295,7 +306,7 @@ fun ClipboardInputLayout(
text = bitmap.exceptionOrNull()?.message ?: "Unknown error",
style = TextStyle(textDirection = TextDirection.Ltr),
color = Color.Red,
fontSize = style.fontSize.spSize(),
fontSize = fontSize,
)
}
} else {
@@ -307,7 +318,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(),
@@ -500,7 +511,7 @@ fun ClipboardInputLayout(
Text(
text = stringRes(R.string.clipboard__empty__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
fontSize = itemStyle.fontSize.spSize() safeTimes 1f,
textAlign = TextAlign.Center,
)
}
@@ -534,7 +545,7 @@ fun ClipboardInputLayout(
Text(
text = stringRes(R.string.clipboard__disabled__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
fontSize = itemStyle.fontSize.spSize() safeTimes 1f,
)
SnyggButton(
modifier = Modifier
@@ -568,7 +579,7 @@ fun ClipboardInputLayout(
Text(
text = stringRes(R.string.clipboard__locked__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
fontSize = itemStyle.fontSize.spSize() safeTimes 1f,
textAlign = TextAlign.Center,
)
}
@@ -577,7 +588,7 @@ fun ClipboardInputLayout(
Column(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
.height(FlorisImeSizing.imeUiHeight()),
) {
HeaderRow()
if (deviceLocked) {

View File

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

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* 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.clipboard
import android.content.ClipData
@@ -27,6 +43,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -87,6 +104,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)
@@ -104,8 +122,8 @@ class FlorisCopyToClipboardActivity : ComponentActivity() {
setContent {
ProvideLocalizedResources(this, forceLayoutDirection = LayoutDirection.Ltr) {
val theme by prefs.advanced.settingsTheme.observeAsState()
val isMaterialYouAware by prefs.advanced.useMaterialYou.observeAsState()
FlorisAppTheme(theme, isMaterialYouAware) {
val accentColor by prefs.advanced.accentColor.observeAsState()
FlorisAppTheme(theme, accentColor.isUnspecified) {
BottomSheet {
Row {
Text(
@@ -145,7 +163,9 @@ class FlorisCopyToClipboardActivity : ComponentActivity() {
Column {
content()
Button(
modifier = Modifier.align(Alignment.End).padding(16.dp),
modifier = Modifier
.align(Alignment.End)
.padding(16.dp),
onClick = { finish() },
colors = ButtonDefaults.textButtonColors(
//containerColor = buttonContainer.background.solidColor(context = context),

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2022-2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

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