Compare commits

..

7 Commits

Author SHA1 Message Date
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
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
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
Patrick Goldinger
80fb20885b Release v0.4.0 2024-09-18 18:26:47 +02:00
459 changed files with 7537 additions and 20993 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,6 +0,0 @@
<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>

View File

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

View File

@@ -114,7 +114,7 @@ Many thanks to [Nikolay Anzarov](https://www.behance.net/nikolayanzarov) ([@Bloo
## License
```
Copyright 2020-2025 The FlorisBoard Contributors
Copyright 2020-2024 Patrick Goldinger
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -129,8 +129,6 @@ 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 -->

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import java.io.ByteArrayOutputStream
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.agp.application)
@@ -23,6 +23,7 @@ plugins {
alias(libs.plugins.kotlin.plugin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mannodermaus.android.junit5)
alias(libs.plugins.mikepenz.aboutlibraries)
}
@@ -51,7 +52,6 @@ android {
freeCompilerArgs = listOf(
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-Xjvm-default=all-compatibility",
"-Xwhen-guards",
)
}
@@ -202,11 +202,16 @@ 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"))
testImplementation(libs.equalsverifier)
testImplementation(libs.kotest.assertions.core)
testImplementation(libs.kotest.extensions.roboelectric)
testImplementation(libs.kotest.property)
testImplementation(libs.kotest.runner.junit5)
androidTestImplementation(libs.androidx.test.ext)
androidTestImplementation(libs.androidx.test.espresso.core)
}

View File

@@ -1,19 +1,3 @@
/*
* 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,4 +1,18 @@
<?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">
@@ -114,7 +128,6 @@
android:name="dev.patrickgold.florisboard.lib.crashutility.CrashDialogActivity"
android:icon="@mipmap/floris_app_icon"
android:label="@string/crash_dialog__title"
android:configChanges="orientation|screenSize"
android:theme="@style/CrashDialogTheme"/>
<!-- Copy to Clipboard Activity -->

View File

@@ -49,12 +49,6 @@
"authors": [ "iamrasel" ],
"direction": "ltr"
},
{
"id": "hindi_in",
"label": "हिंदी",
"authors": [ "npnpatidar" ],
"direction": "ltr"
},
{
"id": "bepo",
"label": "BÉPO",

View File

@@ -1,42 +0,0 @@
[
[
{ "$": "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

@@ -48,7 +48,6 @@
{ "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" ] },
@@ -696,16 +695,6 @@
"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

@@ -1,173 +0,0 @@
{
"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

@@ -0,0 +1,65 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "gboard_day",
"label": "Gboard Day",
"authors": [ "patrickgold", "itskareem" ],
"isNightTheme": false,
"attributes": {
"window": {
"colorPrimary": "#0479ed",
"colorPrimaryDark": "#0467c9",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "true",
"semiTransparentColor": "#20000000",
"textColor": "#000000"
},
"keyboard": {
"background": "#D1D6DC"
},
"key": {
"background": "#FCFFFF",
"backgroundPressed": "#F5F5F5",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "true"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "#FFFFFF",
"foregroundPressed": "#FFFFFF"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#757575"
},
"oneHanded": {
"background": "@keyboard/background",
"foreground": "#424242"
},
"popup": {
"background": "#EEEEEE",
"backgroundActive": "#BDBDBD",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#A000FF",
"foreground": "#FFFFFF"
},
"smartbar": {
"background": "@keyboard/background",
"foreground": "@window/textColor",
"foregroundAlt": "#8A8A8A"
},
"smartbarButton": {
"background": "@key/background",
"foreground": "@key/foreground"
},
"glideTrail": {"foreground": "#200479ed"}
}
}

View File

@@ -0,0 +1,65 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "gboard_night",
"label": "Gboard Night",
"authors": [ "Netscaping" ],
"isNightTheme": true,
"attributes": {
"window": {
"colorPrimary": "#5e97f6",
"colorPrimaryDark": "#4285f4",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "false",
"semiTransparentColor": "#20FFFFFF",
"textColor": "#FFFFFF"
},
"keyboard": {
"background": "#292e33"
},
"key": {
"background": "#484c4f",
"backgroundPressed": "#5e5e60",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "true"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "#FFFFFF",
"foregroundPressed": "#FFFFFF"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#BDBDBD"
},
"oneHanded": {
"background": "#373c41",
"foreground": "#9b9da0"
},
"popup": {
"background": "#373c41",
"backgroundActive": "#5a5e60",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#A000FF",
"foreground": "#FFFFFF"
},
"smartbar": {
"background": "transparent",
"foreground": "#d4d5d6",
"foregroundAlt": "#73FFFFFF"
},
"smartbarButton": {
"background": "#FFFFFF",
"foreground": "#686868"
},
"glideTrail": {"foreground": "#205e97f6"}
}
}

View File

@@ -2,10 +2,10 @@
"$": "ime.extension.theme",
"meta": {
"id": "org.florisboard.themes",
"version": "0.2.0",
"version": "0.1.0",
"title": "FlorisBoard default themes",
"description": "Default themes (both day and night) for the keyboard UI",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>", "lm41 <lm41@lm41.xyz>" ],
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
"license": "apache-2.0"
},
"themes": [
@@ -13,37 +13,49 @@
"id": "floris_day",
"label": "Floris Day",
"authors": [ "patrickgold" ],
"isNight": false
"isNight": false,
"isBorderless": false,
"isMaterialYouAware": false
},
{
"id": "floris_day_borderless",
"label": "Floris Day (Borderless)",
"authors": [ "patrickgold" ],
"isNight": false
"isNight": false,
"isBorderless": true,
"isMaterialYouAware": false
},
{
"id": "floris_night",
"label": "Floris Night",
"authors": [ "patrickgold" ],
"isNight": true
"isNight": true,
"isBorderless": false,
"isMaterialYouAware": false
},
{
"id": "floris_night_borderless",
"label": "Floris Night (Borderless)",
"authors": [ "patrickgold" ],
"isNight": true
"isNight": true,
"isBorderless": true,
"isMaterialYouAware": false
},
{
"id": "floris_pure_night",
"label": "Floris Pure Night",
"authors": [ "serebit" ],
"isNight": true
"isNight": true,
"isBorderless": false,
"isMaterialYouAware": false
},
{
"id": "floris_pure_night_borderless",
"label": "Floris Pure Night (Borderless)",
"authors": [ "serebit" ],
"isNight": true
"isNight": true,
"isBorderless": true,
"isMaterialYouAware": false
}
]
}

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -9,20 +8,7 @@
"--surface": "#ffffff",
"--surface-variant": "#f5f5f5",
"--popup-surface": "#eeeeee",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(0 ,0, 0, 0.25)",
"--one-hand-background": "#e8f5e9",
"--one-hand-foreground": "#424242",
"--incognito-icon-color": "#00000011",
"--on-primary": "#f0f0f0",
"--on-background": "#121212",
"--on-background-disabled": "rgb(175,175,175)",
"--on-surface": "#000000",
"--on-surface-variant": "#5f5f5f",
@@ -30,10 +16,8 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
"keyboard": {
"background": "var(--background)"
},
"key": {
@@ -41,69 +25,48 @@
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp",
"text-max-lines": "1"
"shadow-elevation": "2dp"
},
"key:pressed": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)"
},
"key[code=10]": {
"key[code={c:enter}]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
},
"key[code=10]:pressed": {
"key[code={c:enter}]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code=32]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-family": "monospace",
"font-size": "12sp",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
"font-size": "12sp"
},
"key-popup-box": {
"background": "var(--popup-surface)",
"key-popup": {
"background": "#eeeeee",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -115,180 +78,117 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#12121248"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#12121248"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()"
"shape": "circle()",
"shadow-elevation": "2dp"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
"font-size": "16sp"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--secondary)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"foreground": "var(--on-background)",
"font-size": "16sp"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rectangle()"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-background)"
},
"smartbar-candidate-clip": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-background)"
},
"smartbar-candidate-spacer": {
"foreground": "var(--spacer-color)"
"foreground": "var(--surface)"
},
"clipboard-header": {
"foreground": "var(--on-background)",
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface)",
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"margin": "4dp",
"font-size": "14sp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#eeeeee",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
"emoji-tab": {
"foreground": "var(--on-background)"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
"emoji-tab:focus": {
"foreground": "var(--primary)"
},
"extracted-landscape-input-layout": {
@@ -313,65 +213,15 @@
},
"incognito-mode-indicator": {
"foreground": "var(--incognito-icon-color)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
"foreground": "#00000011"
},
"one-handed-panel": {
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
"background": "#e8f5e9",
"foreground": "#424242"
},
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"system-nav-bar": {
"background": "var(--background)"
}
}

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -9,20 +8,7 @@
"--surface": "#f0f0f0",
"--surface-variant": "#ffffff",
"--popup-surface": "#eeeeee",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(0 ,0, 0, 0.25)",
"--one-hand-background": "#e8f5e9",
"--one-hand-foreground": "#424242",
"--incognito-icon-color": "#00000011",
"--on-primary": "#f0f0f0",
"--on-background": "#121212",
"--on-background-disabled": "#12121248",
"--on-surface": "#000000",
"--on-surface-variant": "#5f5f5f",
@@ -30,82 +16,56 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
"keyboard": {
"background": "var(--background)"
},
"key": {
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"text-max-lines": "1"
"shape": "var(--shape)"
},
"key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"key[code=10]": {
"key[code={c:enter}]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)",
"margin": "0dp 6dp"
"foreground": "var(--on-surface)"
},
"key[code=10]:pressed": {
"key[code={c:enter}]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code=32]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"margin": "0dp 6dp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"font-family": "monospace",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
"font-size": "12sp"
},
"key-popup-box": {
"background": "var(--popup-surface)",
"key-popup": {
"background": "#eeeeee",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -117,6 +77,7 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -124,80 +85,48 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#12121248"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#12121248"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()"
"shape": "circle()",
"shadow-elevation": "2dp"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
"font-size": "16sp"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"font-size": "16sp"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rectangle()"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
@@ -207,11 +136,7 @@
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
@@ -226,80 +151,43 @@
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"margin": "4dp",
"padding": "12dp 8dp",
"font-size": "14sp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#eeeeee",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
"emoji-tab": {
"foreground": "var(--on-background)"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
"emoji-tab:focus": {
"foreground": "var(--primary)"
},
"extracted-landscape-input-layout": {
@@ -324,66 +212,15 @@
},
"incognito-mode-indicator": {
"foreground": "var(--incognito-icon-color)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
"foreground": "#00000011"
},
"one-handed-panel": {
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
"background": "#e8f5e9",
"foreground": "#424242"
},
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"system-nav-bar": {
"background": "var(--background)"
}
}

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -8,16 +7,8 @@
"--background": "#212121",
"--surface": "#424242",
"--surface-variant": "#616161",
"--popup-surface": "#757575",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background": "#dcdcdc",
"--on-background-disabled": "#dcdcdc48",
"--on-surface": "#ffffff",
"--on-surface-variant": "#a0a0a0",
@@ -25,10 +16,8 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
"keyboard": {
"background": "var(--background)"
},
"key": {
@@ -36,68 +25,48 @@
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp",
"text-max-lines": "1"
"shadow-elevation": "2dp"
},
"key:pressed": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)"
},
"key[code=10]": {
"key[code={c:enter}]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
},
"key[code=10]:pressed": {
"key[code={c:enter}]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code=32]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-family": "monospace",
"font-size": "12sp",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
"font-size": "12sp"
},
"key-popup-box": {
"background": "var(--popup-surface)",
"key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -109,180 +78,117 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()"
"shape": "circle()",
"shadow-elevation": "2dp"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
"font-size": "16sp"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--secondary)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"foreground": "var(--on-background)",
"font-size": "16sp"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rectangle()"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-background)"
},
"smartbar-candidate-clip": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-background)"
},
"smartbar-candidate-spacer": {
"foreground": "var(--spacer-color)"
"foreground": "var(--surface)"
},
"clipboard-header": {
"foreground": "var(--on-background)",
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface)",
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"margin": "4dp",
"font-size": "14sp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
"emoji-tab": {
"foreground": "var(--on-background)"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
"emoji-tab:focus": {
"foreground": "var(--primary)"
},
"extracted-landscape-input-layout": {
@@ -307,63 +213,15 @@
},
"incognito-mode-indicator": {
"foreground": "var(--incognito-icon-color)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
"foreground": "#ffffff11"
},
"one-handed-panel": {
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
"background": "#1b5e20",
"foreground": "#eeeeee"
},
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"system-nav-bar": {
"background": "var(--background)"
}
}

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -8,16 +7,8 @@
"--background": "#212121",
"--surface": "#424242",
"--surface-variant": "#616161",
"--popup-surface": "#757575",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background": "#dcdcdc",
"--on-background-disabled": "#dcdcdc48",
"--on-surface": "#ffffff",
"--on-surface-variant": "#a0a0a0",
@@ -25,81 +16,56 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
"keyboard": {
"background": "var(--background)"
},
"key": {
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"text-max-lines": "1"
"shape": "var(--shape)"
},
"key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"key[code=10]": {
"key[code={c:enter}]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)",
"margin": "0dp 6dp"
"foreground": "var(--on-surface)"
},
"key[code=10]:pressed": {
"key[code={c:enter}]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code=32]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"margin": "0dp 6dp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"font-family": "monospace",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
"font-size": "12sp"
},
"key-popup-box": {
"background": "var(--popup-surface)",
"key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -111,6 +77,7 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -118,80 +85,48 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()"
"shape": "circle()",
"shadow-elevation": "2dp"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
"font-size": "16sp"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"font-size": "16sp"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rectangle()"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
@@ -201,11 +136,7 @@
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
@@ -220,80 +151,43 @@
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"margin": "4dp",
"padding": "12dp 8dp",
"font-size": "14sp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
"emoji-tab": {
"foreground": "var(--on-background)"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
"emoji-tab:focus": {
"foreground": "var(--primary)"
},
"extracted-landscape-input-layout": {
@@ -318,63 +212,15 @@
},
"incognito-mode-indicator": {
"foreground": "var(--incognito-icon-color)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
"foreground": "#ffffff11"
},
"one-handed-panel": {
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
"background": "#1b5e20",
"foreground": "#eeeeee"
},
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"system-nav-bar": {
"background": "var(--background)"
}
}

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#388e3c",
"--primary-variant": "#306d32",
@@ -8,15 +7,7 @@
"--background": "#000000",
"--surface": "#212121",
"--surface-variant": "#3d3d3d",
"--popup-surface": "#424242",
"--focused-popup-surface": "#707070",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background-disabled": "#dcdcdc48",
"--on-background": "#eeeeee",
"--on-surface": "#eeeeee",
"--on-surface-variant": "#ffffff73",
@@ -25,10 +16,8 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
"keyboard": {
"background": "var(--background)"
},
"key": {
@@ -36,68 +25,48 @@
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp",
"text-max-lines": "1"
"shadow-elevation": "2dp"
},
"key:pressed": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)"
},
"key[code=10]": {
"key[code={c:enter}]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
},
"key[code=10]:pressed": {
"key[code={c:enter}]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code=32]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-family": "monospace",
"font-size": "12sp",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
"font-size": "12sp"
},
"key-popup-box": {
"background": "var(--popup-surface)",
"key-popup": {
"background": "#424242",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
"key-popup:focus": {
"background": "#707070",
"foreground": "var(--on-surface)"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -109,6 +78,7 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -116,177 +86,109 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()"
"shape": "circle()",
"shadow-elevation": "2dp"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
"font-size": "16sp"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--secondary)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"foreground": "var(--on-background)",
"font-size": "16sp"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rectangle()"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-background)"
},
"smartbar-candidate-clip": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-background)"
},
"smartbar-candidate-spacer": {
"foreground": "var(--spacer-color)"
"foreground": "var(--surface)"
},
"clipboard-header": {
"foreground": "var(--on-background)",
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface)",
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"margin": "4dp",
"font-size": "14sp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
"emoji-tab": {
"foreground": "var(--on-background)"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
"emoji-tab:focus": {
"foreground": "var(--primary)"
},
"extracted-landscape-input-layout": {
@@ -297,77 +199,29 @@
"foreground": "var(--on-background)",
"font-size": "16sp",
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)",
"border-color": "var(--secondary)",
"border-width": "2dp"
"border-color": "var(--secondary-variant)",
"border-width": "1dp"
},
"extracted-landscape-input-action": {
"background": "var(--primary)",
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "rounded-corner(4dp, 4dp, 4dp, 4dp)"
},
"glide-trail": {
"foreground": "var(--primary)"
"foreground": "var(--primary-variant)"
},
"incognito-mode-indicator": {
"foreground": "var(--incognito-icon-color)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
"foreground": "#ffffff11"
},
"one-handed-panel": {
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
"background": "#000000",
"foreground": "#eeeeee"
},
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"system-nav-bar": {
"background": "var(--background)"
}
}

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#388e3c",
"--primary-variant": "#306d32",
@@ -8,15 +7,7 @@
"--background": "#000000",
"--surface": "#212121",
"--surface-variant": "#3d3d3d",
"--popup-surface": "#424242",
"--focused-popup-surface": "#707070",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background-disabled": "#dcdcdc48",
"--on-background": "#eeeeee",
"--on-surface": "#eeeeee",
"--on-surface-variant": "#ffffff73",
@@ -25,81 +16,56 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
"keyboard": {
"background": "var(--background)"
},
"key": {
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"text-max-lines": "1"
"shape": "var(--shape)"
},
"key:pressed": {
"background": "var(--surface)",
"background": "#6161617f",
"foreground": "var(--on-surface)"
},
"key[code=10]": {
"key[code={c:enter}]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)",
"margin": "0dp 6dp"
"foreground": "var(--on-surface)"
},
"key[code=10]:pressed": {
"key[code={c:enter}]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code=32]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"margin": "0dp 6dp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"background": "#61616146",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"font-family": "monospace",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
"font-size": "12sp"
},
"key-popup-box": {
"background": "var(--popup-surface)",
"key-popup": {
"background": "#363636",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
"key-popup:focus": {
"background": "#5F5F5F",
"foreground": "var(--on-surface)"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -111,6 +77,7 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -118,80 +85,48 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"foreground": "var(--on-background-disabled)"
"background": "transparent",
"foreground": "#dcdcdc48"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()"
"shape": "circle()",
"shadow-elevation": "2dp"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
"font-size": "16sp"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"font-size": "16sp"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rectangle()"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
@@ -201,11 +136,7 @@
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
@@ -220,80 +151,43 @@
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"margin": "4dp",
"padding": "12dp 8dp",
"font-size": "14sp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
"emoji-tab": {
"foreground": "var(--on-background)"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
"emoji-tab:focus": {
"foreground": "var(--primary)"
},
"extracted-landscape-input-layout": {
@@ -304,77 +198,29 @@
"foreground": "var(--on-background)",
"font-size": "16sp",
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)",
"border-color": "var(--secondary)",
"border-width": "2dp"
"border-color": "var(--secondary-variant)",
"border-width": "1dp"
},
"extracted-landscape-input-action": {
"background": "var(--primary)",
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"shape": "rounded-corner(4dp, 4dp, 4dp, 4dp)"
},
"glide-trail": {
"foreground": "var(--primary)"
"foreground": "var(--primary-variant)"
},
"incognito-mode-indicator": {
"foreground": "var(--incognito-icon-color)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
"foreground": "#ffffff11"
},
"one-handed-panel": {
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
"background": "#000000",
"foreground": "#eeeeee"
},
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
"system-nav-bar": {
"background": "var(--background)"
}
}

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020-2025 The FlorisBoard Contributors
Copyright 2020 Patrick Goldinger
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-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import dev.patrickgold.florisboard.ime.keyboard.KeyboardManager
import dev.patrickgold.florisboard.ime.media.emoji.FlorisEmojiCompat
import dev.patrickgold.florisboard.ime.nlp.NlpManager
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.lib.cache.CacheManager
import dev.patrickgold.florisboard.lib.crashutility.CrashUtility
@@ -60,6 +61,7 @@ class FlorisApplication : Application() {
System.loadLibrary("fl_native")
} catch (_: Exception) {
}
FlorisImeTheme.init()
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@ 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
@@ -47,11 +46,11 @@ 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
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -79,8 +78,6 @@ 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
@@ -101,7 +98,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
import dev.patrickgold.florisboard.lib.devtools.LogTopic
@@ -109,11 +106,17 @@ 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
import dev.patrickgold.jetpref.datastore.model.observeAsState
import java.lang.ref.WeakReference
import org.florisboard.lib.android.AndroidInternalR
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.isOrientationLandscape
@@ -121,11 +124,7 @@ 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.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggButton
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
import org.florisboard.lib.snygg.ui.rememberSnyggThemeQuery
import java.lang.ref.WeakReference
/**
* Global weak reference for the [FlorisImeService] class. This is needed as certain actions (request hide, switch to
@@ -232,10 +231,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, el.getSubtypeAt(i))
ims.switchInputMethod(el.id)
return true
} else {
ims.window.window?.let { window ->
@@ -267,8 +266,6 @@ 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)
}
@@ -279,20 +276,9 @@ class FlorisImeService : LifecycleInputMethodService() {
WindowCompat.setDecorFitsSystemWindows(window.window!!, false)
subtypeManager.activeSubtypeFlow.collectLatestIn(lifecycleScope) { subtype ->
val config = Configuration(resources.configuration)
if (prefs.localization.displayKeyboardLabelsInSubtypeLanguage.get()) {
config.setLocale(subtype.primaryLocale.base)
}
config.setLocale(subtype.primaryLocale.base)
resourcesContext = createConfigurationContext(config)
}
prefs.localization.displayKeyboardLabelsInSubtypeLanguage.observeForever { shouldSync ->
val config = Configuration(resources.configuration)
if (shouldSync) {
config.setLocale(subtypeManager.activeSubtype.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 {
@@ -331,7 +317,6 @@ class FlorisImeService : LifecycleInputMethodService() {
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(wallpaperChangeReceiver)
FlorisImeServiceReference = WeakReference(null)
inputWindowView = null
}
@@ -462,10 +447,6 @@ class FlorisImeService : LifecycleInputMethodService() {
flogInfo(LogTopic.IMS_EVENTS) { "Creating inline suggestions request" }
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(this)
if (stylesBundle == null) {
flogWarning(LogTopic.IMS_EVENTS) { "Failed to retrieve inline suggestions style bundle" }
return null
}
val spec = InlinePresentationSpec.Builder(
InlineSuggestionUiSmallestSize,
InlineSuggestionUiBiggestSize,
@@ -513,9 +494,7 @@ class FlorisImeService : LifecycleInputMethodService() {
outInsets.visibleTopInsets = visibleTopY
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
val left = 0
val top = if (keyboardManager.activeState.isBottomSheetShowing() || keyboardManager.activeState.isSubtypeSelectionShowing()) {
0
} else {
val top = if (keyboardManager.activeState.isBottomSheetShowing()) { 0 } else {
visibleTopY - if (needAdditionalOverlay) FlorisImeSizing.Static.smartbarHeightPx else 0
}
val right = inputViewSize.width
@@ -528,7 +507,6 @@ class FlorisImeService : LifecycleInputMethodService() {
*/
private fun updateSoftInputWindowLayoutParameters() {
val w = window?.window ?: return
// TODO: setting this to false kinda helps with the nav bar color bug, but introduces padding issues...
WindowCompat.setDecorFitsSystemWindows(w, true)
ViewUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
val layoutHeight = if (isFullscreenUiMode) {
@@ -566,18 +544,19 @@ class FlorisImeService : LifecycleInputMethodService() {
ProvideKeyboardRowBaseHeight {
CompositionLocalProvider(LocalInputFeedbackController provides inputFeedbackController) {
FlorisImeTheme {
// Do not apply system bar padding here yet, we want to draw it ourselves
Column(modifier = Modifier.fillMaxWidth()) {
if (!(isFullscreenUiMode && isExtractUiShown)) {
DevtoolsOverlay(
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
)
) {
DevtoolsUi()
}
}
ImeUi()
SystemUiIme()
}
SystemUiIme()
}
}
}
@@ -588,22 +567,25 @@ class FlorisImeService : LifecycleInputMethodService() {
@Composable
private fun ImeUi() {
val state by keyboardManager.activeState.collectAsState()
val keyboardStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.Keyboard,
mode = state.inputShiftState.value,
)
val layoutDirection = LocalLayoutDirection.current
LaunchedEffect(layoutDirection) {
keyboardManager.activeState.layoutDirection = layoutDirection
SideEffect {
if (keyboardManager.activeState.layoutDirection != layoutDirection) {
keyboardManager.activeState.layoutDirection = layoutDirection
}
}
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
SnyggBox(
elementName = FlorisImeUi.Window.elementName,
attributes = mapOf(FlorisImeUi.Attr.ShiftState to state.inputShiftState.attrName()),
SnyggSurface(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.onGloballyPositioned { coords -> inputViewSize = coords.size },
clickAndSemanticsModifier = Modifier
.onGloballyPositioned { coords -> inputViewSize = coords.size }
// Do not remove below line or touch input may get stuck
.pointerInteropFilter { false },
supportsBackgroundImage = true,
style = keyboardStyle,
) {
val configuration = LocalConfiguration.current
val bottomOffset by if (configuration.isOrientationPortrait()) {
@@ -615,18 +597,17 @@ class FlorisImeService : LifecycleInputMethodService() {
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
// Apply system bars padding here (we already drew our keyboard background)
.safeDrawingPadding()
// FIXME: removing this fixes the Smartbar sizing but breaks one-handed-mode
//.height(IntrinsicSize.Min)
.padding(bottom = bottomOffset),
) {
val oneHandedMode by prefs.keyboard.oneHandedMode.observeAsState()
val oneHandedModeEnabled by prefs.keyboard.oneHandedModeEnabled.observeAsState()
val oneHandedModeScaleFactor by prefs.keyboard.oneHandedModeScaleFactor.observeAsState()
val keyboardWeight = when {
!oneHandedModeEnabled || configuration.isOrientationLandscape() -> 1f
oneHandedMode == OneHandedMode.OFF || configuration.isOrientationLandscape() -> 1f
else -> oneHandedModeScaleFactor / 100f
}
if (oneHandedModeEnabled && oneHandedMode == OneHandedMode.END && configuration.isOrientationPortrait()) {
if (oneHandedMode == OneHandedMode.END && configuration.isOrientationPortrait()) {
OneHandedPanel(
panelSide = OneHandedMode.START,
weight = 1f - keyboardWeight,
@@ -645,7 +626,7 @@ class FlorisImeService : LifecycleInputMethodService() {
}
}
}
if (oneHandedModeEnabled && oneHandedMode == OneHandedMode.START && configuration.isOrientationPortrait()) {
if (oneHandedMode == OneHandedMode.START && configuration.isOrientationPortrait()) {
OneHandedPanel(
panelSide = OneHandedMode.END,
weight = 1f - keyboardWeight,
@@ -656,6 +637,14 @@ 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)
@@ -697,22 +686,12 @@ class FlorisImeService : LifecycleInputMethodService() {
ProvideLocalizedResources(resourcesContext, forceLayoutDirection = LayoutDirection.Ltr) {
FlorisImeTheme {
BottomSheetHostUi(
isShowing = state.isBottomSheetShowing() || state.isSubtypeSelectionShowing(),
isShowing = state.isBottomSheetShowing(),
onHide = {
if (state.isBottomSheetShowing()) {
keyboardManager.activeState.isActionsEditorVisible = false
}
if (state.isSubtypeSelectionShowing()) {
keyboardManager.activeState.isSubtypeSelectionVisible = false
}
keyboardManager.activeState.isActionsEditorVisible = false
},
) {
if (state.isBottomSheetShowing()) {
QuickActionsEditorPanel()
}
if (state.isSubtypeSelectionShowing()) {
SelectSubtypePanel()
}
QuickActionsEditorPanel()
}
}
}
@@ -746,38 +725,44 @@ class FlorisImeService : LifecycleInputMethodService() {
@Composable
fun Content() {
val context = LocalContext.current
ProvideLocalizedResources(resourcesContext, forceLayoutDirection = LayoutDirection.Ltr) {
FlorisImeTheme {
val layoutStyle = FlorisImeTheme.style.get(FlorisImeUi.ExtractedLandscapeInputLayout)
val fieldStyle = FlorisImeTheme.style.get(FlorisImeUi.ExtractedLandscapeInputField)
val actionStyle = FlorisImeTheme.style.get(FlorisImeUi.ExtractedLandscapeInputAction)
val activeEditorInfo by editorInstance.activeInfoFlow.collectAsState()
SnyggBox(FlorisImeUi.ExtractedLandscapeInputLayout.elementName) {
SnyggRow(
Box(
modifier = Modifier
.snyggBackground(context, layoutStyle, FlorisImeTheme.fallbackSurfaceColor()),
) {
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
SnyggBox(FlorisImeUi.ExtractedLandscapeInputLayout.elementName,
val fieldColor = fieldStyle.foreground.solidColor(context, FlorisImeTheme.fallbackContentColor())
AndroidView(
modifier = Modifier
.padding(8.dp)
.fillMaxHeight()
.weight(1f),
) {
val fieldStyle = rememberSnyggThemeQuery(FlorisImeUi.ExtractedLandscapeInputField.elementName)
val foreground = fieldStyle.foreground()
AndroidView(
factory = { extractEditText },
update = { view ->
view.background = null
view.backgroundTintList = null
view.foregroundTintList = null
view.setTextColor(foreground.toArgb())
view.setHintTextColor(foreground.copy(foreground.alpha * 0.6f).toArgb())
view.setTextSize(
TypedValue.COMPLEX_UNIT_SP,
fieldStyle.fontSize(default = 16.sp).value,
)
},
)
}
SnyggButton(
FlorisImeUi.ExtractedLandscapeInputAction.elementName,
.weight(1f)
.snyggShadow(fieldStyle)
.snyggBorder(context, fieldStyle)
.snyggBackground(context, fieldStyle),
factory = { extractEditText },
update = { view ->
view.background = null
view.backgroundTintList = null
view.foregroundTintList = null
view.setTextColor(fieldColor.toArgb())
view.setHintTextColor(fieldColor.copy(fieldColor.alpha * 0.6f).toArgb())
view.setTextSize(
TypedValue.COMPLEX_UNIT_SP,
fieldStyle.fontSize.spSize(default = 16.sp).value,
)
},
)
FlorisButton(
onClick = {
if (activeEditorInfo.extractedActionId != 0) {
currentInputConnection?.performEditorAction(activeEditorInfo.extractedActionId)
@@ -786,13 +771,15 @@ class FlorisImeService : LifecycleInputMethodService() {
}
},
modifier = Modifier.padding(horizontal = 8.dp),
) {
SnyggText(
text = activeEditorInfo.extractedActionLabel
?: getTextForImeAction(activeEditorInfo.imeOptions.action.toInt())
?: "ACTION",
)
}
text = activeEditorInfo.extractedActionLabel
?: getTextForImeAction(activeEditorInfo.imeOptions.action.toInt())
?: "ACTION",
shape = actionStyle.shape.shape(),
colors = ButtonDefaults.buttonColors(
containerColor = actionStyle.background.solidColor(context, FlorisImeTheme.fallbackContentColor()),
contentColor = actionStyle.foreground.solidColor(context, FlorisImeTheme.fallbackSurfaceColor()),
),
)
}
}
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021-2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,10 +18,9 @@ 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
import dev.patrickgold.florisboard.app.settings.theme.SnyggLevel
import dev.patrickgold.florisboard.app.setup.NotificationPermissionState
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.core.Subtype
@@ -41,34 +40,58 @@ import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
import dev.patrickgold.florisboard.ime.smartbar.ExtendedActionsPlacement
import dev.patrickgold.florisboard.ime.smartbar.IncognitoDisplayMode
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickAction
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionArrangement
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionJsonConfig
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
import dev.patrickgold.florisboard.lib.compose.ColorPreferenceSerializer
import org.florisboard.lib.android.isOrientationPortrait
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsTransformingState
import org.florisboard.lib.snygg.SnyggLevel
import dev.patrickgold.florisboard.lib.util.VersionName
import dev.patrickgold.jetpref.datastore.JetPref
import dev.patrickgold.jetpref.datastore.model.PreferenceMigrationEntry
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
import dev.patrickgold.jetpref.datastore.model.PreferenceType
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
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
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
class AppPrefs : PreferenceModel("florisboard-app-prefs") {
val advanced = Advanced()
inner class Advanced {
val settingsTheme = enum(
key = "advanced__settings_theme",
default = AppTheme.AUTO,
)
val useMaterialYou = boolean(
key = "advanced__use_material_you",
default = true,
)
val settingsLanguage = string(
key = "advanced__settings_language",
default = "auto",
)
val showAppIcon = boolean(
key = "advanced__show_app_icon",
default = true,
)
val incognitoMode = enum(
key = "advanced__incognito_mode",
default = IncognitoMode.DYNAMIC_ON_OFF,
)
// Internal pref
val forceIncognitoModeFromDynamic = boolean(
key = "advanced__force_incognito_mode_from_dynamic",
default = false,
)
}
val clipboard = Clipboard()
inner class Clipboard {
val useInternalClipboard = boolean(
@@ -95,14 +118,6 @@ 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,
@@ -115,14 +130,6 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "clipboard__clear_primary_clip_deletes_last_item",
default = true,
)
val suggestionEnabled = boolean(
key = "clipboard__suggestion_enabled",
default = true,
)
val suggestionTimeout = int(
key = "clipboard__suggestion_timeout",
default = 60,
)
}
val correction = Correction()
@@ -151,6 +158,10 @@ 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,
@@ -483,11 +494,7 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
)
val oneHandedMode = enum(
key = "keyboard__one_handed_mode",
default = OneHandedMode.END,
)
val oneHandedModeEnabled = boolean(
key = "keyboard__one_handed_mode_enabled",
default = false,
default = OneHandedMode.OFF,
)
val oneHandedModeScaleFactor = int(
key = "keyboard__one_handed_mode_scale_factor",
@@ -559,14 +566,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
@Composable
fun fontSizeMultiplier(): Float {
val configuration = LocalConfiguration.current
val oneHandedModeEnabled by oneHandedModeEnabled.observeAsState()
val oneHandedMode by oneHandedMode.observeAsState()
val oneHandedModeFactor by oneHandedModeScaleFactor.observeAsTransformingState { it / 100.0f }
val fontSizeMultiplierBase by if (configuration.isOrientationPortrait()) {
fontSizeMultiplierPortrait
} else {
fontSizeMultiplierLandscape
}.observeAsTransformingState { it / 100.0f }
val fontSizeMultiplier = fontSizeMultiplierBase * if (oneHandedModeEnabled && configuration.isOrientationPortrait()) {
val fontSizeMultiplier = fontSizeMultiplierBase * if (oneHandedMode != OneHandedMode.OFF && configuration.isOrientationPortrait()) {
oneHandedModeFactor
} else {
1.0f
@@ -581,10 +588,6 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "localization__display_language_names_in",
default = DisplayLanguageNamesIn.SYSTEM_LOCALE,
)
val displayKeyboardLabelsInSubtypeLanguage = boolean(
key = "localization__display_keyboard_labels_in_subtype_language",
default = false,
)
val activeSubtypeId = long(
key = "localization__active_subtype_id",
default = Subtype.DEFAULT.id,
@@ -595,30 +598,6 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
)
}
val other = Other()
inner class Other {
val settingsTheme = enum(
key = "other__settings_theme",
default = AppTheme.AUTO,
)
val accentColor = custom(
key = "other__accent_color",
default = when (AndroidVersion.ATLEAST_API31_S) {
true -> Color.Unspecified
false -> DEFAULT_GREEN
},
serializer = ColorPreferenceSerializer,
)
val settingsLanguage = string(
key = "other__settings_language",
default = "auto",
)
val showAppIcon = boolean(
key = "other__show_app_icon",
default = true,
)
}
val smartbar = Smartbar()
inner class Smartbar {
val enabled = boolean(
@@ -695,14 +674,13 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "suggestion__block_possibly_offensive",
default = true,
)
val incognitoMode = enum(
key = "suggestion__incognito_mode",
default = IncognitoMode.DYNAMIC_ON_OFF,
val clipboardContentEnabled = boolean(
key = "suggestion__clipboard_content_enabled",
default = true,
)
// Internal pref
val forceIncognitoModeFromDynamic = boolean(
key = "suggestion__force_incognito_mode_from_dynamic",
default = false,
val clipboardContentTimeout = int(
key = "suggestion__clipboard_content_timeout",
default = 60,
)
}
@@ -722,14 +700,6 @@ 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),
@@ -738,9 +708,9 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
// key = "theme__sunset_time",
// default = LocalTime.of(18, 0),
//)
val editorColorRepresentation = enum(
key = "theme__editor_color_representation",
default = ColorRepresentation.HEX,
val editorDisplayColorsAs = enum(
key = "theme__editor_display_colors_as",
default = DisplayColorsAs.HEX8,
)
val editorDisplayKbdAfterDialogs = enum(
key = "theme__editor_display_kbd_after_dialogs",
@@ -754,6 +724,34 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
override fun migrate(entry: PreferenceMigrationEntry): PreferenceMigrationEntry {
return when (entry.key) {
// Migrate enums from their lowercase to uppercase representation
// Keep migration rule until: 0.5 dev cycle
"advanced__settings_theme", "gestures__swipe_up", "gestures__swipe_down", "gestures__swipe_left",
"gestures__swipe_right", "gestures__space_bar_swipe_up", "gestures__space_bar_swipe_left",
"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", "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",
-> {
entry.transform(rawValue = entry.rawValue.uppercase())
}
// Migrate old private mode force flag as this is a sensitive preference
// Keep migration rule until: 0.5 dev cycle
"advanced__force_private_mode" -> {
if (entry.rawValue.toBoolean()) {
entry.transform(
type = PreferenceType.string(),
key = "advanced__incognito_mode",
rawValue = IncognitoMode.FORCE_ON.toString(),
)
} else {
entry.reset()
}
}
// Migrate media prefs to emoji prefs
// Keep migration rule until: 0.6 dev cycle
@@ -768,74 +766,18 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
"media__emoji_recently_used_max_size" -> {
entry.transform(key = "emoji__history_recent_max_size")
}
// Migrate advanced prefs to other prefs
// Keep migration rules until: 0.7 dev cycle
"advanced__settings_theme" -> {
entry.transform(key = "other__settings_theme")
}
"advanced__accent_color" -> {
entry.transform(key = "other__accent_color")
}
"advanced__settings_language" -> {
entry.transform(key = "other__settings_language")
}
"advanced__show_app_icon" -> {
entry.transform(key = "other__show_app_icon")
}
"advanced__incognito_mode" -> {
entry.transform(key = "suggestion__incognito_mode")
}
"advanced__force_incognito_mode_from_dynamic" -> {
entry.transform(key = "suggestion__force_incognito_mode_from_dynamic")
}
// Migrate clipboard suggestion prefs to clipboard
// Keep migration rules until: 0.7 dev cycle
"suggestion__clipboard_content_enabled" -> {
entry.transform(key = "clipboard__suggestion_enabled")
}
"suggestion__clipboard_content_timeout" -> {
entry.transform(key = "clipboard__suggestion_timeout")
}
//Migrate one hand mode prefs keep until: 0.7 dev cycle
"keyboard__one_handed_mode" -> {
if (entry.rawValue != "OFF") {
val prefs by florisPreferenceModel()
prefs.keyboard.oneHandedModeEnabled.set(true)
entry.keepAsIs()
} else {
entry.reset()
}
}
"smartbar__action_arrangement" -> {
val arrangement = QuickActionJsonConfig.decodeFromString<QuickActionArrangement>(entry.rawValue)
val newArrangement = arrangement.copy(
dynamicActions = arrangement.dynamicActions.map { action ->
if (action is QuickAction.InsertKey && action.data.code == KeyCode.COMPACT_LAYOUT_TO_RIGHT) {
action.copy(TextKeyData.TOGGLE_COMPACT_LAYOUT)
} else {
action
}
}
)
val json = QuickActionJsonConfig.encodeToString(newArrangement)
entry.transform(rawValue = json)
}
// Migrate theme editor fine-tuning
// Keep migration rule until: 0.6 dev cycle
"theme__editor_display_colors_as" -> {
val colorRepresentation = when (entry.rawValue) {
"RGBA" -> ColorRepresentation.RGB
else -> ColorRepresentation.HEX
}
"media__emoji_preferred_skin_tone" -> {
entry.transform(
key = "theme__editor_color_representation",
rawValue = colorRepresentation.name,
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,25 +1,9 @@
/*
* 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
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.settings.theme.DisplayColorsAs
import dev.patrickgold.florisboard.app.settings.theme.DisplayKbdAfterDialogs
import dev.patrickgold.florisboard.app.settings.theme.SnyggLevel
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.input.CapitalizationBehavior
import dev.patrickgold.florisboard.ime.input.HapticVibrationMode
@@ -41,9 +25,9 @@ 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.lib.compose.stringRes
import org.florisboard.lib.snygg.SnyggLevel
import dev.patrickgold.jetpref.datastore.ui.ListPreferenceEntry
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
import org.florisboard.lib.kotlin.curlyFormat
import kotlin.reflect.KClass
@@ -58,19 +42,19 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
)
entry(
key = AppTheme.AUTO_AMOLED,
label = stringRes(R.string.pref__other__settings_theme__auto_amoled),
label = stringRes(R.string.pref__advanced__settings_theme__auto_amoled),
)
entry(
key = AppTheme.LIGHT,
label = stringRes(R.string.pref__other__settings_theme__light),
label = stringRes(R.string.pref__advanced__settings_theme__light),
)
entry(
key = AppTheme.DARK,
label = stringRes(R.string.pref__other__settings_theme__dark),
label = stringRes(R.string.pref__advanced__settings_theme__dark),
)
entry(
key = AppTheme.AMOLED_DARK,
label = stringRes(R.string.pref__other__settings_theme__amoled_dark),
label = stringRes(R.string.pref__advanced__settings_theme__amoled_dark),
)
}
},
@@ -102,24 +86,18 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
)
}
},
ColorRepresentation::class to DEFAULT to {
DisplayColorsAs::class to DEFAULT to {
listPrefEntries {
entry(
key = ColorRepresentation.HEX,
label = stringRes(R.string.enum__color_representation__hex),
key = DisplayColorsAs.HEX8,
label = stringRes(R.string.enum__display_colors_as__hex8),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "#4caf50ff"),
showDescriptionOnlyIfSelected = true,
)
entry(
key = ColorRepresentation.RGB,
label = stringRes(R.string.enum__color_representation__rgb),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "rgba(76, 175, 80, 1.0)"),
showDescriptionOnlyIfSelected = true,
)
entry(
key = ColorRepresentation.HSV,
label = stringRes(R.string.enum__color_representation__hsv),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "hsva(122, 56, 68, 1.0)"),
key = DisplayColorsAs.RGBA,
label = stringRes(R.string.enum__display_colors_as__rgba),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "rgba(76,175,80,1.0)"),
showDescriptionOnlyIfSelected = true,
)
}
@@ -382,6 +360,10 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
},
OneHandedMode::class to DEFAULT to {
listPrefEntries {
entry(
key = OneHandedMode.OFF,
label = stringRes(R.string.enum__one_handed_mode__off),
)
entry(
key = OneHandedMode.START,
label = stringRes(R.string.enum__one_handed_mode__start),
@@ -544,10 +526,6 @@ 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-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -88,17 +88,17 @@ class FlorisAppActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
prefs.other.settingsTheme.observe(this) {
prefs.advanced.settingsTheme.observe(this) {
appTheme = it
}
prefs.other.settingsLanguage.observe(this) {
prefs.advanced.settingsLanguage.observe(this) {
val config = Configuration(resources.configuration)
val locale = if (it == "auto") FlorisLocale.default() else FlorisLocale.fromTag(it)
config.setLocale(locale.base)
resourcesContext = createConfigurationContext(config)
}
if (AndroidVersion.ATMOST_API28_P) {
prefs.other.showAppIcon.observe(this) {
prefs.advanced.showAppIcon.observe(this) {
showAppIcon = it
}
}
@@ -117,7 +117,8 @@ class FlorisAppActivity : ComponentActivity() {
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
setContent {
ProvideLocalizedResources(resourcesContext) {
FlorisAppTheme(theme = appTheme) {
val useMaterialYou by prefs.advanced.useMaterialYou.observeAsState()
FlorisAppTheme(theme = appTheme, isMaterialYouAware = useMaterialYou) {
Surface(color = MaterialTheme.colorScheme.background) {
AppContent()
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,7 +47,7 @@ import dev.patrickgold.florisboard.app.settings.HomeScreen
import dev.patrickgold.florisboard.app.settings.about.AboutScreen
import dev.patrickgold.florisboard.app.settings.about.ProjectLicenseScreen
import dev.patrickgold.florisboard.app.settings.about.ThirdPartyLicensesScreen
import dev.patrickgold.florisboard.app.settings.advanced.OtherScreen
import dev.patrickgold.florisboard.app.settings.advanced.AdvancedScreen
import dev.patrickgold.florisboard.app.settings.advanced.BackupScreen
import dev.patrickgold.florisboard.app.settings.advanced.RestoreScreen
import dev.patrickgold.florisboard.app.settings.clipboard.ClipboardScreen
@@ -110,9 +110,9 @@ object Routes {
const val Media = "settings/media"
const val Other = "settings/other"
const val Backup = "settings/other/backup"
const val Restore = "settings/other/restore"
const val Advanced = "settings/advanced"
const val Backup = "settings/advanced/backup"
const val Restore = "settings/advanced/restore"
const val About = "settings/about"
const val ProjectLicense = "settings/about/project-license"
@@ -239,7 +239,7 @@ object Routes {
composableWithDeepLink(Settings.Media) { MediaScreen() }
composableWithDeepLink(Settings.Other) { OtherScreen() }
composableWithDeepLink(Settings.Advanced) { AdvancedScreen() }
composableWithDeepLink(Settings.Backup) { BackupScreen() }
composableWithDeepLink(Settings.Restore) { RestoreScreen() }

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,22 +17,19 @@
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,
@@ -73,66 +70,170 @@ 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,
)
@Composable
fun getColorScheme(
context: Context,
theme: AppTheme,
): ColorScheme {
val prefs by florisPreferenceModel()
val accentColor by prefs.other.accentColor.observeAsState()
val isDark = isSystemInDarkTheme()
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,
)
return when (theme) {
AppTheme.AUTO -> {
if (isDark) {
ColorMappings.dynamicDarkColorScheme(context, accentColor)
} else {
ColorMappings.dynamicLightColorScheme(context, accentColor)
}
}
AppTheme.DARK -> {
ColorMappings.dynamicDarkColorScheme(context, accentColor)
}
AppTheme.LIGHT -> {
ColorMappings.dynamicLightColorScheme(context, accentColor)
}
AppTheme.AMOLED_DARK -> {
ColorMappings.dynamicDarkColorScheme(context, accentColor).amoled()
}
AppTheme.AUTO_AMOLED -> {
if (isDark) {
ColorMappings.dynamicDarkColorScheme(context, accentColor).amoled()
} else {
ColorMappings.dynamicLightColorScheme(context, accentColor)
}
}
}
}
fun ColorScheme.amoled(): ColorScheme {
return this.copy(background = Color.Black, surface = Color.Black)
}
private val amoledScheme = darkScheme.copy(
background = amoledDark,
surface = amoledDark
)
@Composable
fun FlorisAppTheme(
theme: AppTheme,
content: @Composable () -> Unit,
isMaterialYouAware: Boolean,
content: @Composable () -> Unit
) {
val colors = getColorScheme(
context = LocalContext.current,
theme = theme,
)
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 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-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -43,18 +42,12 @@ import androidx.compose.ui.unit.sp
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.clipboardManager
import dev.patrickgold.florisboard.editorInstance
import dev.patrickgold.florisboard.ime.keyboard.CachedLayout
import dev.patrickgold.florisboard.ime.keyboard.DebugLayoutComputationResult
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofill
import dev.patrickgold.florisboard.ime.theme.ThemeManager
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.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.snygg.SnyggMissingSchemaException
import java.text.SimpleDateFormat
import java.util.*
@@ -63,44 +56,30 @@ 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 themeManager by context.themeManager()
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()
val themeInfo by themeManager.activeThemeInfo.observeAsState()
CompositionLocalProvider(
LocalContentColor provides Color.White,
LocalLayoutDirection provides LayoutDirection.Ltr,
) {
Column(modifier = modifier) {
if (devtoolsEnabled && showPrimaryClip) {
if (showPrimaryClip) {
DevtoolsClipboardOverlay()
}
if (devtoolsEnabled && showInputStateOverlay) {
if (showInputStateOverlay) {
DevtoolsInputStateOverlay()
}
if (debugLayoutResult?.allLayoutsSuccess() == false) {
DevtoolsLastLayoutComputationOverlay(debugLayoutResult)
}
if (devtoolsEnabled && showSpellingOverlay) {
if (showSpellingOverlay) {
DevtoolsSpellingOverlay()
}
if (devtoolsEnabled && showInlineAutofillOverlay && AndroidVersion.ATLEAST_API30_R) {
if (showInlineAutofillOverlay && AndroidVersion.ATLEAST_API30_R) {
DevtoolsInlineAutofillOverlay()
}
val loadFailure = themeInfo?.loadFailure
if (loadFailure != null) {
DevtoolsStylesheetFailedToLoadOverlay(loadFailure)
}
}
}
}
@@ -146,34 +125,6 @@ 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() {
val context = LocalContext.current
@@ -235,39 +186,6 @@ private fun DevtoolsInlineAutofillOverlay() {
}
}
@Composable
private fun DevtoolsStylesheetFailedToLoadOverlay(loadFailure: ThemeManager.LoadFailure) {
DevtoolsOverlayBox(title = "Failed to load stylesheet, fell back to base style") {
DevtoolsSubGroup(title = "Extension") {
DevtoolsText(text = "id: ${loadFailure.extension.id}")
DevtoolsText(text = "title: ${loadFailure.extension.title}")
DevtoolsText(text = "version: ${loadFailure.extension.version}")
}
DevtoolsSubGroup(title = "Component") {
DevtoolsText(text = "id: ${loadFailure.component.id}")
DevtoolsText(text = "label: ${loadFailure.component.label}")
DevtoolsText(text = "path: ${loadFailure.component.stylesheetPath()}")
}
val cause = loadFailure.cause
DevtoolsSubGroup(title = "Cause") {
DevtoolsText(text = "${cause.message}")
}
if (cause is SnyggMissingSchemaException) {
DevtoolsSubGroup(title = "Explanation") {
DevtoolsText(
text = """
It appears youre trying to load a theme designed for FlorisBoard v0.4 (Snygg v1), which isnt compatible with the latest release using Snygg v2.
If you are the theme author, please update your theme to support Snygg v2.
If youre a user, please update your theme via the Addons Store. If an updated version isnt available yet, please select one of the built-in themes during this transition period.
""".trimIndent()
)
}
}
}
}
@Composable
private fun DevtoolsOverlayBox(
title: String,

View File

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

View File

@@ -1,19 +1,3 @@
/*
* 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
@@ -21,7 +5,6 @@ import androidx.compose.foundation.layout.Spacer
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.Input
import androidx.compose.material.icons.filled.Shop
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material3.MaterialTheme
@@ -30,8 +13,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
@@ -44,41 +25,6 @@ import dev.patrickgold.florisboard.lib.ext.generateUpdateUrl
import dev.patrickgold.florisboard.lib.util.launchUrl
import org.florisboard.lib.kotlin.curlyFormat
@Composable
fun ImportExtensionBox(navController: NavController) {
val context = LocalContext.current
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
Text(
modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 4.dp),
text = stringRes(id = R.string.ext__home__info),
style = MaterialTheme.typography.bodySmall,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
) {
FlorisTextButton(
onClick = {
context.launchUrl("https://${BuildConfig.FLADDONS_STORE_URL}/")
},
icon = Icons.Default.Shop,
text = stringRes(id = R.string.ext__home__visit_store),
)
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, null))
},
icon = Icons.AutoMirrored.Filled.Input,
text = stringRes(R.string.action__import),
)
}
}
}
@Composable
fun UpdateBox(extensionIndex: List<Extension>) {
val context = LocalContext.current

View File

@@ -1,19 +1,3 @@
/*
* 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-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,11 +81,14 @@ fun ExtensionComponentView(
when (component) {
is ThemeExtensionComponent -> {
val text = remember(
component.authors, component.isNightTheme, component.stylesheetPath(),
component.authors, component.isNightTheme, component.isBorderless,
component.isMaterialYouAware, component.stylesheetPath(),
) {
buildString {
appendLine("authors = ${component.authors}")
appendLine("isNightTheme = ${component.isNightTheme}")
appendLine("isBorderless = ${component.isBorderless}")
appendLine("isMaterialYouAware = ${component.isMaterialYouAware}")
append("stylesheetPath = ${component.stylesheetPath()}")
}
}

View File

@@ -1,290 +0,0 @@
/*
* 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 android.provider.OpenableColumns
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Photo
import androidx.compose.material.icons.filled.TextFields
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.MimeTypeFilter
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.lib.cache.CacheManager
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import java.io.File
import java.util.*
import org.florisboard.lib.android.query
import org.florisboard.lib.android.readToFile
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.kotlin.io.parentDir
import org.florisboard.lib.kotlin.io.subDir
import org.florisboard.lib.kotlin.io.subFile
const val FONTS = "fonts"
const val IMAGES = "images"
val MIME_TYPES = mapOf(
FONTS to listOf(
// Source: https://www.alienfactory.co.uk/articles/mime-types-for-web-fonts-in-bedsheet#mimeTypes
"font/*",
"application/vnd.ms-fontobject", // .eot
"application/font-woff", // .woff
"application/x-font-truetype", // .ttf
"application/x-font-opentype", // .otf
),
IMAGES to listOf(
"image/*",
),
)
@Composable
fun ExtensionEditFilesScreen(workspace: CacheManager.ExtEditorWorkspace<*>) = FlorisScreen {
title = stringRes(R.string.ext__editor__files__title)
fun handleBackPress() {
workspace.currentAction = null
}
navigationIcon {
FlorisIconButton(
onClick = { handleBackPress() },
icon = Icons.Default.Close,
)
}
content {
val context = LocalContext.current
var version by rememberSaveable { mutableIntStateOf(0) }
val fontFiles = remember(version) {
workspace.extDir.subDir(FONTS).listFiles { it.isFile }.orEmpty().asList()
}
val imageFiles = remember(version) {
workspace.extDir.subDir(IMAGES).listFiles { it.isFile }.orEmpty().asList()
}
var currentImportDest by remember { mutableStateOf<String?>(null) }
var currentImportResult by remember { mutableStateOf<Result<Pair<File, String>>?>(null) }
val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri ->
currentImportResult = runCatching {
checkNotNull(uri) { "" }
val tempFile = context.cacheDir.subFile("temp_${UUID.randomUUID()}")
context.contentResolver.readToFile(uri, tempFile)
val mimeType = context.contentResolver.getType(uri)
val types = MIME_TYPES[currentImportDest!!]!!
checkNotNull(MimeTypeFilter.matches(mimeType, types.toTypedArray())) {
"Given file mime type was '$mimeType', expected one of $types"
}
val fileName = context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME)).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return@use null
val name = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.getString(name)
}
tempFile to fileName.orEmpty()
}
},
)
LaunchedEffect(currentImportResult) {
val message = currentImportResult?.exceptionOrNull()?.message
if (!message.isNullOrBlank()) {
context.showLongToast(message)
}
}
BackHandler {
handleBackPress()
}
@Composable
fun FileList(title: String, icon: ImageVector, files: List<File>, onAdd: () -> Unit) {
var dialogFile by remember { mutableStateOf<File?>(null) }
ListItem(
headlineContent = {
Text(
text = title,
color = MaterialTheme.colorScheme.secondary,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
leadingContent = {
Spacer(modifier = Modifier.width(24.dp))
},
trailingContent = {
IconButton(
onClick = onAdd,
) {
Icon(Icons.Default.Add, null)
}
},
)
for (file in files) {
Preference(
onClick = {
dialogFile = file
},
icon = icon,
title = file.name,
)
}
dialogFile?.let { file ->
var fileNameInput by rememberSaveable { mutableStateOf(file.name) }
JetPrefAlertDialog(
title = "Rename or remove",
confirmLabel = stringRes(R.string.action__apply),
dismissLabel = stringRes(R.string.action__cancel),
neutralLabel = stringRes(R.string.action__delete),
allowOutsideDismissal = true,
onNeutral = {
if (file.delete()) {
context.showShortToast("Successfully deleted")
} else {
context.showShortToast("Failed to remove")
}
dialogFile = null
version++
},
onConfirm = {
val newFile = file.parentFile!!.subFile(fileNameInput).canonicalFile
if (newFile.parentFile != file.canonicalFile.parentFile) {
context.showLongToast("Invalid file name!")
return@JetPrefAlertDialog
}
if (newFile.exists()) {
context.showShortToast("Filename already exists.")
return@JetPrefAlertDialog
}
val success = file.renameTo(newFile)
if (success) {
context.showShortToast("Successfully renamed")
} else {
context.showShortToast("Failed to rename the file.")
}
dialogFile = null
version++
},
onDismiss = {
dialogFile = null
},
) {
JetPrefTextField(
value = fileNameInput,
onValueChange = { fileNameInput = it },
singleLine = true,
)
}
}
}
FileList(
title = FONTS.replaceFirstChar { it.uppercase() },
icon = Icons.Default.TextFields,
files = fontFiles,
) {
currentImportDest = FONTS
importLauncher.launch("*/*")
}
FileList(
title = IMAGES.replaceFirstChar { it.uppercase() },
icon = Icons.Default.Photo,
files = imageFiles,
) {
currentImportDest = IMAGES
importLauncher.launch("*/*")
}
val dest = currentImportDest
val result = currentImportResult?.getOrNull()
if (dest != null && result != null) {
var fileNameInput by rememberSaveable { mutableStateOf(result.second) }
JetPrefAlertDialog(
title = "Import ${dest.substring(0, dest.length - 1)}",
confirmLabel = stringRes(R.string.action__add),
onConfirm = {
val fileName = fileNameInput.trim()
val dir = workspace.extDir.subDir(dest)
dir.mkdirs()
val file = dir.subFile(fileName)
if (file.parentDir != workspace.extDir.subDir(dest)) {
context.showShortToast("Invalid file name")
} else if (file.exists()) {
context.showShortToast("File already exists")
} else {
val tempFile = result.first
if (!tempFile.renameTo(file)) {
context.showShortToast("Failed to rename file")
tempFile.delete()
}
currentImportDest = null
currentImportResult = null
version++
}
},
dismissLabel = stringRes(R.string.action__cancel),
onDismiss = {
val tempFile = result.first
tempFile.delete()
currentImportDest = null
currentImportResult = null
},
) {
JetPrefTextField(
value = fileNameInput,
onValueChange = { fileNameInput = it },
singleLine = true,
)
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ 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.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -48,7 +49,6 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.settings.advanced.RadioListItem
import dev.patrickgold.florisboard.app.settings.theme.DialogProperty
import dev.patrickgold.florisboard.app.settings.theme.PrettyPrintConfig
import dev.patrickgold.florisboard.app.settings.theme.ThemeEditorScreen
import dev.patrickgold.florisboard.cacheManager
import dev.patrickgold.florisboard.extensionManager
@@ -64,9 +64,9 @@ import dev.patrickgold.florisboard.lib.compose.FlorisButtonBar
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisUnsavedChangesDialog
import dev.patrickgold.florisboard.lib.compose.Validation
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.Extension
@@ -83,17 +83,15 @@ import dev.patrickgold.florisboard.lib.ext.validate
import dev.patrickgold.florisboard.lib.io.FlorisRef
import dev.patrickgold.florisboard.lib.io.ZipUtils
import dev.patrickgold.florisboard.lib.rememberValidationResult
import org.florisboard.lib.snygg.SnyggStylesheetJsonConfig
import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import java.util.*
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.kotlin.io.deleteContentsRecursively
import org.florisboard.lib.kotlin.io.subDir
import org.florisboard.lib.kotlin.io.subFile
import org.florisboard.lib.kotlin.io.writeJson
import java.util.UUID
import kotlin.reflect.KClass
private val TextFieldVerticalPadding = 8.dp
@@ -199,7 +197,7 @@ private fun ExtensionEditScreenSheetSwitcher(
ManageDependenciesScreen(workspace)
}
is EditorAction.ManageFiles -> {
ExtensionEditFilesScreen(workspace)
ManageFilesScreen(workspace)
}
is EditorAction.CreateComponent<*> -> {
CreateComponentScreen(workspace, action.type)
@@ -263,33 +261,17 @@ private fun EditScreen(
return
}
val manifest = extEditor.build()
workspace.saverDir.deleteContentsRecursively()
val manifestFile = workspace.saverDir.subFile(ExtensionDefaults.MANIFEST_FILE_NAME)
manifestFile.writeJson(manifest, ExtensionJsonConfig)
when (extEditor) {
is ThemeExtensionEditor -> {
// TODO: this is hacky
val fonts = workspace.extDir.subDir("fonts")
if (fonts.exists()) {
fonts.copyRecursively(workspace.saverDir.subDir("fonts"), overwrite = true)
}
val images = workspace.extDir.subDir("images")
if (images.exists()) {
images.copyRecursively(workspace.saverDir.subDir("images"), overwrite = true)
}
for (theme in extEditor.themes) {
val stylesheetFile = workspace.saverDir.subFile(theme.stylesheetPath())
stylesheetFile.parentFile?.mkdirs()
val stylesheetEditor = theme.stylesheetEditor
if (stylesheetEditor != null) {
runCatching {
val stylesheet = stylesheetEditor.build().toJson(PrettyPrintConfig).getOrThrow()
stylesheetFile.writeText(stylesheet)
}.onFailure {
// TODO: better error handling
context.showLongToast(it.message.toString())
return
}
val stylesheet = stylesheetEditor.build()
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
} else {
val unmodifiedStylesheetFile = workspace.extDir.subFile(theme.stylesheetPath())
if (unmodifiedStylesheetFile.exists()) {
@@ -600,6 +582,35 @@ private fun ManageDependenciesScreen(workspace: CacheManager.ExtEditorWorkspace<
}
}
@Composable
private fun ManageFilesScreen(workspace: CacheManager.ExtEditorWorkspace<*>) = FlorisScreen {
title = stringRes(R.string.ext__editor__files__title)
fun handleBackPress() {
workspace.currentAction = null
}
navigationIcon {
FlorisIconButton(
onClick = { handleBackPress() },
icon = Icons.Default.Close,
)
}
content {
BackHandler {
handleBackPress()
}
FlorisInfoCard(
modifier = Modifier.padding(all = 8.dp),
text = """
Managing archive files is currently not supported.
""".trimIndent().replace('\n', ' '),
)
}
}
private enum class CreateFrom {
EMPTY,
EXISTING;
@@ -692,14 +703,15 @@ private fun <T : ExtensionComponent> CreateComponentScreen(
val component = editor.themes.find { it.id == componentName.componentId } ?: return
val componentEditor = component.let { c ->
ThemeExtensionComponentEditor(
componentId, c.label, c.authors, c.isNightTheme, stylesheetPath = "",
componentId, c.label, c.authors, c.isNightTheme, c.isBorderless,
c.isMaterialYouAware, stylesheetPath = "",
).also { it.stylesheetEditor = c.stylesheetEditor }
}
if (componentEditor.stylesheetEditor != null) {
val stylesheet = componentEditor.stylesheetEditor!!.build()
val stylesheetFile = workspace.extDir.subFile(componentEditor.stylesheetPath())
stylesheetFile.parentFile?.mkdirs()
val stylesheet = componentEditor.stylesheetEditor!!.build().toJson(PrettyPrintConfig).getOrThrow()
stylesheetFile.writeText(stylesheet)
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
componentEditor.stylesheetEditor = null
} else {
val srcStylesheetFile = workspace.extDir.subFile(component.stylesheetPath())
@@ -801,37 +813,36 @@ private fun <T : ExtensionComponent> CreateComponentScreen(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.ext__meta__id),
) {
JetPrefTextField(
modifier = Modifier.fillMaxWidth(),
FlorisOutlinedTextField(
value = newId,
onValueChange = { newId = it },
singleLine = true,
showValidationError = showValidationErrors,
validationResult = newIdValidation,
)
Validation(showValidationErrors, newIdValidation)
}
DialogProperty(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.ext__meta__label),
) {
JetPrefTextField(
modifier = Modifier.fillMaxWidth(),
FlorisOutlinedTextField(
value = newLabel,
onValueChange = { newLabel = it },
singleLine = true,
showValidationError = showValidationErrors,
validationResult = newLabelValidation,
)
Validation(showValidationErrors, newLabelValidation)
}
DialogProperty(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.ext__meta__authors),
) {
JetPrefTextField(
modifier = Modifier.fillMaxWidth(),
FlorisOutlinedTextField(
value = newAuthors,
onValueChange = { newAuthors = it },
showValidationError = showValidationErrors,
validationResult = newAuthorsValidation,
)
Validation(showValidationErrors, newAuthorsValidation)
}
}
}
@@ -849,6 +860,7 @@ private fun EditorSheetTextField(
showValidationError: Boolean = false,
validationResult: ValidationResult? = null,
) {
val borderColor = MaterialTheme.colorScheme.outline
Column(modifier = Modifier.padding(vertical = TextFieldVerticalPadding)) {
Row(
modifier = Modifier
@@ -868,13 +880,18 @@ private fun EditorSheetTextField(
)
}
}
JetPrefTextField(
FlorisOutlinedTextField(
modifier = modifier.fillMaxWidth(),
enabled = enabled,
value = value,
onValueChange = onValueChange,
singleLine = singleLine,
showValidationError = showValidationError,
validationResult = validationResult,
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = borderColor,
disabledBorderColor = borderColor,
)
)
Validation(showValidationError, validationResult)
}
}

View File

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

View File

@@ -1,33 +1,32 @@
/*
* 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
import androidx.compose.foundation.layout.Spacer
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.Input
import androidx.compose.material.icons.filled.Keyboard
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Shop
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.jetpref.datastore.ui.Preference
@Composable
@@ -41,7 +40,36 @@ fun ExtensionHomeScreen() = FlorisScreen {
val extensionIndex = extensionManager.combinedExtensionList()
content {
ImportExtensionBox(navController)
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
Text(
modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 4.dp),
text = stringRes(id = R.string.ext__home__info),
style = MaterialTheme.typography.bodySmall,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
) {
FlorisTextButton(
onClick = {
context.launchUrl("https://${BuildConfig.FLADDONS_STORE_URL}/")
},
icon = Icons.Default.Shop,
text = stringRes(id = R.string.ext__home__visit_store),
)
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, null))
},
icon = Icons.AutoMirrored.Filled.Input,
text = stringRes(R.string.action__import),
)
}
}
UpdateBox(extensionIndex = extensionIndex)

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 The FlorisBoard Contributors
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,15 +17,10 @@
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
@@ -38,13 +33,8 @@ 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
@@ -56,7 +46,6 @@ 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
@@ -91,69 +80,49 @@ 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 {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.florisScrollbar(state = listState, isVertical = true),
state = listState,
contentPadding = PaddingValues(bottom = fabHeightDp),
) {
if (showUpdate) {
item {
ImportExtensionBox(navController)
}
item {
UpdateBox(extensionIndex = extensionIndex)
}
}
items(extensionIndex) { ext ->
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
title = ext.meta.title,
subtitle = ext.meta.id,
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),
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
text = ext.meta.description ?: "",
style = MaterialTheme.typography.bodySmall,
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),
)
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),
)
}
}
}
}
@@ -173,9 +142,6 @@ 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-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.app.settings
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Assignment
import androidx.compose.material.icons.filled.Adb
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.Gesture
import androidx.compose.material.icons.filled.Language
@@ -153,8 +152,8 @@ fun HomeScreen() = FlorisScreen {
)
Preference(
icon = Icons.Outlined.Build,
title = stringRes(R.string.settings__other__title),
onClick = { navController.navigate(Routes.Settings.Other) },
title = stringRes(R.string.settings__advanced__title),
onClick = { navController.navigate(Routes.Settings.Advanced) },
)
Preference(
icon = Icons.Outlined.Info,

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,71 +19,58 @@ 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.FormatColorFill
import androidx.compose.material.icons.filled.FormatPaint
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
import dev.patrickgold.florisboard.app.Routes
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 org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.color.ColorMappings
import dev.patrickgold.jetpref.datastore.ui.vectorResource
@Composable
fun OtherScreen() = FlorisScreen {
title = stringRes(R.string.settings__other__title)
fun AdvancedScreen() = FlorisScreen {
title = stringRes(R.string.settings__advanced__title)
previewFieldVisible = false
val navController = LocalNavController.current
val context = LocalContext.current
content {
ListPreference(
prefs.other.settingsTheme,
prefs.advanced.settingsTheme,
icon = Icons.Default.Palette,
title = stringRes(R.string.pref__other__settings_theme__label),
title = stringRes(R.string.pref__advanced__settings_theme__label),
entries = enumDisplayEntriesOf(AppTheme::class),
)
ColorPickerPreference(
pref = prefs.other.accentColor,
title = stringRes(R.string.pref__other__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
}
}
SwitchPreference(
pref = prefs.advanced.useMaterialYou,
icon = Icons.Default.FormatPaint,
title = stringRes(R.string.pref__advanced__settings_material_you__label),
visibleIf = {
AndroidVersion.ATLEAST_API31_S
},
)
ListPreference(
prefs.other.settingsLanguage,
prefs.advanced.settingsLanguage,
icon = Icons.Default.Language,
title = stringRes(R.string.pref__other__settings_language__label),
title = stringRes(R.string.pref__advanced__settings_language__label),
entries = listPrefEntries {
listOf(
"auto",
@@ -145,15 +132,21 @@ fun OtherScreen() = FlorisScreen {
}
)
SwitchPreference(
prefs.other.showAppIcon,
prefs.advanced.showAppIcon,
icon = Icons.Default.Preview,
title = stringRes(R.string.pref__other__show_app_icon__label),
title = stringRes(R.string.pref__advanced__show_app_icon__label),
summary = when {
AndroidVersion.ATLEAST_API29_Q -> stringRes(R.string.pref__other__show_app_icon__summary_atleast_q)
AndroidVersion.ATLEAST_API29_Q -> stringRes(R.string.pref__advanced__show_app_icon__summary_atleast_q)
else -> null
},
enabledIf = { AndroidVersion.ATMOST_API28_P },
)
ListPreference(
prefs.advanced.incognitoMode,
icon = vectorResource(id = R.drawable.ic_incognito),
title = stringRes(R.string.pref__advanced__incognito_mode__label),
entries = enumDisplayEntriesOf(IncognitoMode::class),
)
Preference(
icon = Icons.Default.Adb,
title = stringRes(R.string.devtools__title),

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@ 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
@@ -52,23 +51,6 @@ fun ClipboardScreen() = FlorisScreen {
enabledIf = { prefs.clipboard.useInternalClipboard isEqualTo true },
)
PreferenceGroup(title = stringRes(R.string.pref__clipboard__group_clipboard_suggestion__label)) {
SwitchPreference(
prefs.clipboard.suggestionEnabled,
title = stringRes(R.string.pref__clipboard__suggestion_enabled__label),
summary = stringRes(R.string.pref__clipboard__suggestion_enabled__summary),
)
DialogSliderPreference(
prefs.clipboard.suggestionTimeout,
title = stringRes(R.string.pref__clipboard__suggestion_timeout__label),
valueLabel = { stringRes(R.string.pref__clipboard__suggestion_timeout__summary, "v" to it) },
min = 30,
max = 300,
stepIncrement = 5,
enabledIf = { prefs.clipboard.suggestionEnabled isEqualTo true },
)
}
PreferenceGroup(title = stringRes(R.string.pref__clipboard__group_clipboard_history__label)) {
SwitchPreference(
prefs.clipboard.historyEnabled,
@@ -89,22 +71,6 @@ 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-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -56,15 +56,14 @@ import dev.patrickgold.florisboard.ime.dictionary.UserDictionaryEntry
import dev.patrickgold.florisboard.ime.dictionary.UserDictionaryValidation
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.Validation
import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.rememberValidationResult
import dev.patrickgold.florisboard.lib.util.launchActivity
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.florisboard.lib.android.showLongToast
@@ -367,35 +366,39 @@ fun UserDictionaryScreen(type: UserDictionaryType) = FlorisScreen {
) {
Column {
DialogProperty(text = stringRes(R.string.settings__udm__dialog__word_label)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = word,
onValueChange = { word = it },
showValidationError = showValidationErrors,
validationResult = wordValidation,
)
Validation(showValidationErrors, wordValidation)
}
DialogProperty(text = stringRes(
R.string.settings__udm__dialog__freq_label,
"f_min" to FREQUENCY_MIN, "f_max" to FREQUENCY_MAX,
)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = freq,
onValueChange = { freq = it },
showValidationError = showValidationErrors,
validationResult = freqValidation,
)
Validation(showValidationErrors, freqValidation)
}
DialogProperty(text = stringRes(R.string.settings__udm__dialog__shortcut_label)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = shortcut,
onValueChange = { shortcut = it },
showValidationError = showValidationErrors,
validationResult = shortcutValidation,
)
Validation(showValidationErrors, shortcutValidation)
}
DialogProperty(text = stringRes(R.string.settings__udm__dialog__locale_label)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = locale,
onValueChange = { locale = it },
showValidationError = showValidationErrors,
validationResult = localeValidation,
)
Validation(showValidationErrors, localeValidation)
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -107,10 +107,8 @@ fun KeyboardScreen() = FlorisScreen {
PreferenceGroup(title = stringRes(R.string.pref__keyboard__group_layout__label)) {
ListPreference(
prefs.keyboard.oneHandedMode,
prefs.keyboard.oneHandedModeEnabled,
title = stringRes(R.string.pref__keyboard__one_handed_mode__label),
entries = enumDisplayEntriesOf(OneHandedMode::class),
summarySwitchDisabled = stringRes(R.string.state__disabled),
)
DialogSliderPreference(
prefs.keyboard.oneHandedModeScaleFactor,
@@ -119,7 +117,7 @@ fun KeyboardScreen() = FlorisScreen {
min = 70,
max = 90,
stepIncrement = 1,
enabledIf = { prefs.keyboard.oneHandedModeEnabled.isTrue() },
enabledIf = { prefs.keyboard.oneHandedMode isNotEqualTo OneHandedMode.OFF },
)
ListPreference(
prefs.keyboard.landscapeInputUiMode,

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -53,7 +53,6 @@ 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.datastore.ui.SwitchPreference
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -104,10 +103,6 @@ fun LocalizationScreen() = FlorisScreen {
title = stringRes(R.string.settings__localization__display_language_names_in__label),
entries = enumDisplayEntriesOf(DisplayLanguageNamesIn::class),
)
SwitchPreference(
prefs.localization.displayKeyboardLabelsInSubtypeLanguage,
title = stringRes(R.string.settings__localization__display_keyboard_labels_in_subtype_language),
)
Preference(
title = stringRes(R.string.settings__localization__language_pack_title),
summary = stringRes(R.string.settings__localization__language_pack_summary),

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,17 +25,12 @@ 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
@@ -78,7 +73,6 @@ 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
@@ -184,7 +178,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
@@ -235,7 +229,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
@Composable
fun SubtypePropertyDropdown(
title: String,
layoutType: LayoutType,
layoutType: LayoutType
) {
SubtypeProperty(title) {
SubtypeLayoutDropdown(
@@ -333,7 +327,6 @@ 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 {
@@ -488,30 +481,20 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
showSubtypePresetsDialog = false
},
) {
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),
)
}
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,
)
}
HorizontalDivider()
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 The FlorisBoard Contributors
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,18 +19,10 @@ 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
@@ -39,11 +31,8 @@ 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
@@ -52,11 +41,6 @@ fun MediaScreen() = FlorisScreen {
previewFieldVisible = true
iconSpaceReserved = true
val prefs by florisPreferenceModel()
var shouldDelete by remember { mutableStateOf<ShouldDelete?>(null) }
val scope = rememberCoroutineScope()
content {
ListPreference(
prefs.emoji.preferredSkinTone,
@@ -101,21 +85,6 @@ fun MediaScreen() = FlorisScreen {
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)) {
@@ -169,49 +138,4 @@ fun MediaScreen() = FlorisScreen {
)
}
}
DeleteEmojiHistoryConfirmDialog(
shouldDelete = shouldDelete,
onDismiss = {
shouldDelete = null
},
onConfirm = {
shouldDelete?.let {
scope.launch {
if (it.pinned) {
EmojiHistoryHelper.deletePinned(prefs = prefs)
} else {
EmojiHistoryHelper.deleteHistory(prefs = prefs)
}
}
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-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +14,12 @@
* limitations under the License.
*/
package org.florisboard.lib.kotlin
package dev.patrickgold.florisboard.app.settings.theme
import kotlin.reflect.KClass
fun KClass<*>.simpleNameOrEnclosing(): String? {
return if (this.simpleName == "Companion") {
// Companion object => get the enclosing class
this.java.enclosingClass.simpleName
} else {
// Normal object => directly get class
this.simpleName
}
/**
* DisplayColorsAs indicates how color strings should be visually presented to the user.
*/
enum class DisplayColorsAs {
HEX8,
RGBA;
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,8 +48,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -59,6 +59,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -81,33 +82,30 @@ import dev.patrickgold.florisboard.ime.keyboard.computeImageVector
import dev.patrickgold.florisboard.ime.keyboard.computeLabel
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.ime.theme.FlorisImeUiSpec
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.NATIVE_NULLPTR
import dev.patrickgold.florisboard.lib.compose.FlorisChip
import dev.patrickgold.florisboard.lib.compose.FlorisHyperlinkText
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.util.InputMethodUtils
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefDropdown
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import dev.patrickgold.jetpref.material.ui.JetPrefTextFieldDefaults
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.stringRes
import org.florisboard.lib.kotlin.curlyFormat
import org.florisboard.lib.snygg.SnyggAnnotationRule
import org.florisboard.lib.snygg.SnyggElementRule
import dev.patrickgold.florisboard.lib.compose.FlorisChip
import dev.patrickgold.florisboard.lib.compose.FlorisDropdownMenu
import dev.patrickgold.florisboard.lib.compose.FlorisHyperlinkText
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.snygg.SnyggRule
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.NonNullSaver
import dev.patrickgold.florisboard.lib.util.InputMethodUtils
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import org.florisboard.lib.kotlin.curlyFormat
import org.florisboard.lib.kotlin.getKeyByValue
private val TransparentTextSelectionColors = TextSelectionColors(
handleColor = Color.Transparent,
backgroundColor = Color.Transparent,
)
internal val SnyggEmptyRuleForAdding = SnyggElementRule(elementName = "--select--")
internal val SnyggEmptyRuleForAdding = SnyggRule(element = "- select -")
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -118,69 +116,69 @@ internal fun EditRuleDialog(
onDeleteRule: (rule: SnyggRule) -> Unit,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val isAddRuleDialog = initRule == SnyggEmptyRuleForAdding
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
var showAlreadyExistsError by rememberSaveable { mutableStateOf(false) }
val possibleRuleTemplates = remember {
buildList {
add(SnyggEmptyRuleForAdding)
add(SnyggAnnotationRule.Font(fontName = ""))
FlorisImeUi.elementNames.forEach { name ->
add(SnyggElementRule(name))
}
}
}
val possibleRuleLabels = possibleRuleTemplates.map { rule ->
val elementName = when (rule) {
is SnyggElementRule -> rule.elementName
else -> rule.decl().name
}
context.translateElementName(elementName, level) ?: rule
val possibleElementNames = remember {
listOf(SnyggEmptyRuleForAdding.element) + FlorisImeUiSpec.elements.keys
}
val possibleElementLabels = possibleElementNames.map { translateElementName(it, level) ?: it }
var elementsExpanded by remember { mutableStateOf(false) }
var elementsSelectedIndex by rememberSaveable {
val index = possibleRuleTemplates
.indexOfFirst { rule ->
val elementName = when (rule) {
is SnyggElementRule -> rule.elementName
else -> rule.decl().name
}
val initElementName = when (initRule) {
is SnyggElementRule -> initRule.elementName
else -> initRule.decl().name
}
elementName == initElementName
}
.coerceIn(possibleRuleTemplates.indices)
val index = possibleElementNames.indexOf(initRule.element).coerceIn(possibleElementNames.indices)
mutableIntStateOf(index)
}
var currentRule by rememberSaveable(elementsSelectedIndex, stateSaver = SnyggRule.NonNullSaver) {
mutableStateOf(
if (isAddRuleDialog) possibleRuleTemplates[elementsSelectedIndex] else initRule
)
val codes = rememberSaveable(saver = IntListSaver) { initRule.codes.toMutableStateList() }
var editCodeDialogValue by rememberSaveable { mutableStateOf<Int?>(null) }
val groups = rememberSaveable(saver = IntListSaver) { initRule.groups.toMutableStateList() }
var shiftStateUnshifted by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.UNSHIFTED.value))
}
var shiftStateShiftedManual by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.SHIFTED_MANUAL.value))
}
var shiftStateShiftedAutomatic by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.SHIFTED_AUTOMATIC.value))
}
var shiftStateCapsLock by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.CAPS_LOCK.value))
}
var pressedSelector by rememberSaveable { mutableStateOf(initRule.pressedSelector) }
var focusSelector by rememberSaveable { mutableStateOf(initRule.focusSelector) }
var disabledSelector by rememberSaveable { mutableStateOf(initRule.disabledSelector) }
JetPrefAlertDialog(
title = stringRes(
if (isAddRuleDialog) {
R.string.settings__theme_editor__add_rule
} else {
R.string.settings__theme_editor__edit_rule
}
),
confirmLabel = stringRes(
if (isAddRuleDialog) {
R.string.action__add
} else {
R.string.action__apply
}
),
title = stringRes(if (isAddRuleDialog) {
R.string.settings__theme_editor__add_rule
} else {
R.string.settings__theme_editor__edit_rule
}),
confirmLabel = stringRes(if (isAddRuleDialog) {
R.string.action__add
} else {
R.string.action__apply
}),
onConfirm = {
if (isAddRuleDialog && elementsSelectedIndex == 0) {
showSelectAsError = true
} else {
if (!onConfirmRule(initRule, currentRule)) {
val newRule = SnyggRule(
element = possibleElementNames[elementsSelectedIndex],
codes = codes.toList(),
groups = groups.toList(),
shiftStates = buildList {
if (shiftStateUnshifted) { add(InputShiftState.UNSHIFTED.value) }
if (shiftStateShiftedManual) { add(InputShiftState.SHIFTED_MANUAL.value) }
if (shiftStateShiftedAutomatic) { add(InputShiftState.SHIFTED_AUTOMATIC.value) }
if (shiftStateCapsLock) { add(InputShiftState.CAPS_LOCK.value) }
},
pressedSelector = pressedSelector,
focusSelector = focusSelector,
disabledSelector = disabledSelector,
)
if (!onConfirmRule(initRule, newRule)) {
showAlreadyExistsError = true
}
}
@@ -204,228 +202,154 @@ internal fun EditRuleDialog(
)
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_name)) {
JetPrefDropdown(
options = possibleRuleLabels,
selectedOptionIndex = elementsSelectedIndex,
onSelectOption = { elementsSelectedIndex = it },
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_element)) {
FlorisDropdownMenu(
items = possibleElementLabels,
expanded = elementsExpanded,
enabled = isAddRuleDialog,
selectedIndex = elementsSelectedIndex,
isError = showSelectAsError && elementsSelectedIndex == 0,
onSelectItem = { elementsSelectedIndex = it },
onExpandRequest = { elementsExpanded = true },
onDismissRequest = { elementsExpanded = false },
)
}
(currentRule as? SnyggAnnotationRule.Font)?.apply {
DialogProperty(text = stringRes(R.string.snygg__rule_annotation__font_name)) {
JetPrefTextField(
modifier = Modifier,
value = fontName,
onValueChange = {
currentRule = copy(fontName = it)
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_selectors)) {
Row(modifier = Modifier.florisHorizontalScroll()) {
FlorisChip(
onClick = { pressedSelector = !pressedSelector },
modifier = Modifier.padding(end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.PRESSED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__pressed)
},
singleLine = true,
selected = pressedSelector,
)
FlorisChip(
onClick = { focusSelector = !focusSelector },
modifier = Modifier.padding( end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.FOCUS_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__focus)
},
selected = focusSelector,
)
FlorisChip(
onClick = { disabledSelector = !disabledSelector },
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.DISABLED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__disabled)
},
selected = disabledSelector,
)
}
}
// TODO: Move to toplevel @Composable function
(currentRule as? SnyggElementRule)?.apply {
if (elementName == SnyggEmptyRuleForAdding.elementName) {
return@apply
}
fun updateCurrentRule(newSelector: SnyggSelector) {
currentRule = if (selector == newSelector) {
copy(selector = SnyggSelector.NONE)
DialogProperty(
text = stringRes(R.string.settings__theme_editor__rule_codes),
trailingIconTitle = {
FlorisIconButton(
onClick = { editCodeDialogValue = NATIVE_NULLPTR.toInt() },
modifier = Modifier.offset(x = 12.dp),
icon = Icons.Default.Add,
)
},
) {
Text(
modifier = Modifier.padding(vertical = 4.dp),
text = stringRes(if (codes.isEmpty()) {
R.string.settings__theme_editor__no_codes_defined
} else {
copy(selector = newSelector)
}
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_selectors)) {
Row(modifier = Modifier.florisHorizontalScroll()) {
//TODO: LazyRow
R.string.settings__theme_editor__codes_defined
}),
fontStyle = FontStyle.Italic,
)
FlowRow {
for (code in codes) {
FlorisChip(
onClick = { updateCurrentRule(SnyggSelector.PRESSED) },
modifier = Modifier.padding(end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.PRESSED.id
else -> stringRes(R.string.snygg__rule_selector__pressed)
},
selected = selector == SnyggSelector.PRESSED,
)
FlorisChip(
onClick = { updateCurrentRule(SnyggSelector.FOCUS) },
modifier = Modifier.padding( end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.FOCUS.id
else -> stringRes(R.string.snygg__rule_selector__focus)
},
selected = selector == SnyggSelector.FOCUS,
)
FlorisChip(
onClick = { updateCurrentRule(SnyggSelector.HOVER) },
modifier = Modifier.padding( end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.HOVER.id
else -> stringRes(R.string.snygg__rule_selector__hover)
},
selected = selector == SnyggSelector.HOVER,
)
FlorisChip(
onClick = { updateCurrentRule(SnyggSelector.DISABLED) },
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.DISABLED.id
else -> stringRes(R.string.snygg__rule_selector__disabled)
},
selected = selector == SnyggSelector.DISABLED,
onClick = { editCodeDialogValue = code },
text = code.toString(),
selected = editCodeDialogValue == code,
shape = MaterialTheme.shapes.medium,
)
}
}
}
val codes = remember(currentRule) {
attributes[FlorisImeUi.Attr.Code] ?: emptyList()
}
var editCodeDialogValue by rememberSaveable { mutableStateOf<String?>(null) }
val initCodeValue = editCodeDialogValue
if (initCodeValue != null) {
EditCodeValueDialog(
codeValue = initCodeValue,
checkExisting = { codes.contains(it) },
onAdd = {
currentRule = copy(
attributes = attributes.including(FlorisImeUi.Attr.Code to it)
)
},
onDelete = {
currentRule = copy(
attributes = attributes.excluding(FlorisImeUi.Attr.Code to it)
)
},
onDismiss = { editCodeDialogValue = null },
)
}
DialogProperty(
text = stringRes(R.string.settings__theme_editor__rule_codes),
trailingIconTitle = {
FlorisIconButton(
onClick = { editCodeDialogValue = KeyCode.UNSPECIFIED.toString() },
modifier = Modifier.offset(x = 12.dp),
icon = Icons.Default.Add,
)
},
) {
Text(
modifier = Modifier.padding(vertical = 4.dp),
text = stringRes(
if (codes.isEmpty()) {
R.string.settings__theme_editor__no_codes_defined
} else {
R.string.settings__theme_editor__codes_defined
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_shift_states)) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
FlorisChip(
onClick = { shiftStateUnshifted = !shiftStateUnshifted },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.UNSHIFTED.value)
}
),
fontStyle = FontStyle.Italic,
else -> stringRes(R.string.enum__input_shift_state__unshifted)
},
selected = shiftStateUnshifted,
)
FlorisChip(
onClick = { shiftStateShiftedManual = !shiftStateShiftedManual },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.SHIFTED_MANUAL.value)
}
else -> stringRes(R.string.enum__input_shift_state__shifted_manual)
},
selected = shiftStateShiftedManual,
)
FlorisChip(
onClick = { shiftStateShiftedAutomatic = !shiftStateShiftedAutomatic },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.SHIFTED_AUTOMATIC.value)
}
else -> stringRes(R.string.enum__input_shift_state__shifted_automatic)
},
selected = shiftStateShiftedAutomatic,
)
FlorisChip(
onClick = { shiftStateCapsLock = !shiftStateCapsLock },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.CAPS_LOCK.value)
}
else -> stringRes(R.string.enum__input_shift_state__caps_lock)
},
selected = shiftStateCapsLock,
)
FlowRow {
for (code in codes) {
FlorisChip(
onClick = { editCodeDialogValue = code },
text = code.toString(),
selected = editCodeDialogValue == code,
shape = MaterialTheme.shapes.medium,
)
}
}
}
val shiftStateUnshifted = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.UNSHIFTED.attrName()) == true
}
val shiftStateShiftedManual = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.SHIFTED_MANUAL.attrName()) == true
}
val shiftStateShiftedAutomatic = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.SHIFTED_AUTOMATIC.attrName()) == true
}
val shiftStateCapsLock = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.CAPS_LOCK.attrName()) == true
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_shift_states)) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.UNSHIFTED.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.UNSHIFTED.attrName()
else -> stringRes(R.string.enum__input_shift_state__unshifted)
},
selected = shiftStateUnshifted,
)
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.SHIFTED_MANUAL.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.SHIFTED_MANUAL.attrName()
else -> stringRes(R.string.enum__input_shift_state__shifted_manual)
},
selected = shiftStateShiftedManual,
)
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.SHIFTED_AUTOMATIC.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.SHIFTED_AUTOMATIC.attrName()
else -> stringRes(R.string.enum__input_shift_state__shifted_automatic)
},
selected = shiftStateShiftedAutomatic,
)
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.CAPS_LOCK.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.CAPS_LOCK.attrName()
else -> stringRes(R.string.enum__input_shift_state__caps_lock)
},
selected = shiftStateCapsLock,
)
}
}
}
}
}
val initCodeValue = editCodeDialogValue
if (initCodeValue != null) {
EditCodeValueDialog(
codeValue = initCodeValue,
checkExisting = { codes.contains(it) },
onAdd = { codes.add(it) },
onDelete = { codes.remove(it) },
onDismiss = { editCodeDialogValue = null },
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EditCodeValueDialog(
codeValue: String,
checkExisting: (String) -> Boolean,
onAdd: (String) -> Unit,
onDelete: (String) -> Unit,
codeValue: Int,
checkExisting: (Int) -> Boolean,
onAdd: (Int) -> Unit,
onDelete: (Int) -> Unit,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
var inputCodeString by rememberSaveable(codeValue) {
val str = if (codeValue == KeyCode.UNSPECIFIED.toString()) "" else codeValue.toString()
val str = if (codeValue == 0) "" else codeValue.toString()
mutableStateOf(str)
}
val textKeyData = remember(inputCodeString) {
@@ -482,7 +406,6 @@ private fun EditCodeValueDialog(
inputCodeString = data.code.toString()
isRecordingKey = false
}
override fun onInputKeyRepeat(data: KeyData) = Unit
override fun onInputKeyCancel(data: KeyData) = Unit
}
@@ -500,20 +423,16 @@ private fun EditCodeValueDialog(
}
JetPrefAlertDialog(
title = stringRes(
if (codeValue == KeyCode.UNSPECIFIED.toString()) {
R.string.settings__theme_editor__add_code
} else {
R.string.settings__theme_editor__edit_code
}
),
confirmLabel = stringRes(
if (codeValue == KeyCode.UNSPECIFIED.toString()) {
R.string.action__add
} else {
R.string.action__apply
}
),
title = stringRes(if (codeValue == NATIVE_NULLPTR.toInt()) {
R.string.settings__theme_editor__add_code
} else {
R.string.settings__theme_editor__edit_code
}),
confirmLabel = stringRes(if (codeValue == NATIVE_NULLPTR.toInt()) {
R.string.action__add
} else {
R.string.action__apply
}),
onConfirm = {
val code = inputCodeString.trim().toIntOrNull(radix = 10)
when {
@@ -521,28 +440,25 @@ private fun EditCodeValueDialog(
errorId = R.string.settings__theme_editor__code_invalid
showError = true
}
code.toString() == codeValue -> {
code == codeValue -> {
onDismiss()
}
checkExisting(code.toString()) -> {
checkExisting(code) -> {
errorId = R.string.settings__theme_editor__code_already_exists
showError = true
}
else -> {
if (codeValue != KeyCode.UNSPECIFIED.toString()) {
if (codeValue != NATIVE_NULLPTR.toInt()) {
onDelete(codeValue)
}
onAdd(code.toString())
onAdd(code)
onDismiss()
}
}
},
dismissLabel = stringRes(R.string.action__cancel),
onDismiss = onDismiss,
neutralLabel = if (codeValue != KeyCode.UNSPECIFIED.toString()) {
neutralLabel = if (codeValue != NATIVE_NULLPTR.toInt()) {
stringRes(R.string.action__delete)
} else {
null
@@ -589,7 +505,7 @@ private fun EditCodeValueDialog(
LocalTextSelectionColors.current
}
CompositionLocalProvider(LocalTextSelectionColors provides textSelectionColors) {
JetPrefTextField(
FlorisOutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester)
.weight(1f),
@@ -598,7 +514,7 @@ private fun EditCodeValueDialog(
inputCodeString = v
showError = false
},
placeholderText = when {
placeholder = when {
isRecordingKey -> {
stringRes(R.string.settings__theme_editor__code_recording_placeholder)
}
@@ -611,25 +527,21 @@ private fun EditCodeValueDialog(
},
isError = showError,
singleLine = true,
appearance = JetPrefTextFieldDefaults.filled(
colors = if (isRecordingKey) {
TextFieldDefaults.colors(
focusedTextColor = Color.Transparent,
cursorColor = Color.Transparent,
)
} else {
TextFieldDefaults.colors()
}
),
trailingIcon = {
FlorisIconButton(
onClick = { requestStartRecording() },
icon = Icons.Default.Pageview,
iconColor = recordingKeyColor,
colors = if (isRecordingKey) {
OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Transparent,
cursorColor = Color.Transparent,
)
}
} else {
OutlinedTextFieldDefaults.colors()
},
)
}
FlorisIconButton(
onClick = { requestStartRecording() },
icon = Icons.Default.Pageview,
iconColor = recordingKeyColor,
)
}
AnimatedVisibility(visible = showError) {
Text(
@@ -661,12 +573,9 @@ private fun TextKeyDataPreviewBox(
override val mode = KeyboardMode.NUMERIC_ADVANCED
override fun getKeyForPos(pointerX: Float, pointerY: Float) = error("not implemented")
override fun keys() = error("not implemented")
override fun layout(
keyboardWidth: Float, keyboardHeight: Float, desiredKey: Key,
extendTouchBoundariesDownwards: Boolean,
) = error("not implemented")
override fun layout(keyboardWidth: Float, keyboardHeight: Float, desiredKey: Key,
extendTouchBoundariesDownwards: Boolean) = error("not implemented")
}
override fun context() = context
}
}
@@ -694,13 +603,7 @@ private fun TextKeyDataPreviewBox(
.align(Alignment.CenterVertically),
contentAlignment = Alignment.Center,
) {
if (icon != null) {
Icon(
modifier = Modifier.requiredSize(24.dp),
imageVector = icon,
contentDescription = null,
)
} else if (label != null) {
if (label != null) {
Text(
text = label,
fontSize = 16.sp,
@@ -708,6 +611,13 @@ private fun TextKeyDataPreviewBox(
softWrap = false,
)
}
if (icon != null) {
Icon(
modifier = Modifier.requiredSize(24.dp),
imageVector = icon,
contentDescription = null,
)
}
}
Column(modifier = Modifier.weight(1f)) {
Text(text = displayName)

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,9 +23,9 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.SnyggLevel
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.PreferenceLayout
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
private val FineTuneContentPadding = PaddingValues(horizontal = 8.dp)
@@ -44,9 +44,9 @@ fun FineTuneDialog(onDismiss: () -> Unit) {
entries = enumDisplayEntriesOf(SnyggLevel::class),
)
ListPreference(
listPref = prefs.theme.editorColorRepresentation,
title = stringRes(R.string.settings__theme_editor__fine_tune__color_representation),
entries = enumDisplayEntriesOf(ColorRepresentation::class),
listPref = prefs.theme.editorDisplayColorsAs,
title = stringRes(R.string.settings__theme_editor__fine_tune__display_colors_as),
entries = enumDisplayEntriesOf(DisplayColorsAs::class),
)
ListPreference(
listPref = prefs.theme.editorDisplayKbdAfterDialogs,

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,64 +26,29 @@ import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.FormatAlignLeft
import androidx.compose.material.icons.automirrored.filled.FormatAlignRight
import androidx.compose.material.icons.automirrored.filled.WrapText
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.CheckBox
import androidx.compose.material.icons.filled.CheckBoxOutlineBlank
import androidx.compose.material.icons.filled.FontDownload
import androidx.compose.material.icons.filled.FormatAlignCenter
import androidx.compose.material.icons.filled.FormatAlignJustify
import androidx.compose.material.icons.filled.FormatBold
import androidx.compose.material.icons.filled.FormatItalic
import androidx.compose.material.icons.filled.FormatSize
import androidx.compose.material.icons.filled.FormatStrikethrough
import androidx.compose.material.icons.filled.FormatUnderlined
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.OpenInFull
import androidx.compose.material.icons.filled.Padding
import androidx.compose.material.icons.filled.Straighten
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.snygg.value.SnyggCutCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggDefinedVarValue
import org.florisboard.lib.snygg.value.SnyggDpSizeValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggShapeValue
import org.florisboard.lib.snygg.value.SnyggStaticColorValue
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
import org.florisboard.lib.snygg.value.SnyggSpSizeValue
import org.florisboard.lib.snygg.value.SnyggValue
import dev.patrickgold.jetpref.material.ui.checkeredBackground
import org.florisboard.lib.color.ColorMappings
import org.florisboard.lib.color.getColor
import org.florisboard.lib.snygg.value.SnyggContentScaleValue
import org.florisboard.lib.snygg.value.SnyggCustomFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggDynamicDarkColorValue
import org.florisboard.lib.snygg.value.SnyggDynamicLightColorValue
import org.florisboard.lib.snygg.value.SnyggFontStyleValue
import org.florisboard.lib.snygg.value.SnyggFontWeightValue
import org.florisboard.lib.snygg.value.SnyggGenericFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggNoValue
import org.florisboard.lib.snygg.value.SnyggPaddingValue
import org.florisboard.lib.snygg.value.SnyggTextAlignValue
import org.florisboard.lib.snygg.value.SnyggTextDecorationLineValue
import org.florisboard.lib.snygg.value.SnyggTextOverflowValue
import org.florisboard.lib.snygg.value.SnyggUriValue
import org.florisboard.lib.snygg.value.SnyggYesValue
object SnyggValueIcon {
interface Spec {
@@ -121,51 +86,13 @@ internal fun SnyggValueIcon(
modifier: Modifier = Modifier,
spec: SnyggValueIcon.Spec = SnyggValueIcon.Normal,
) {
val prefs by florisPreferenceModel()
val context = LocalContext.current
val accentColor by prefs.theme.accentColor.observeAsState()
when (value) {
is SnyggStaticColorValue -> {
is SnyggSolidColorValue -> {
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = value.color)
}
is SnyggDynamicLightColorValue -> {
val colorScheme = ColorMappings.dynamicLightColorScheme(context, accentColor)
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = colorScheme.getColor(value.colorName))
}
is SnyggDynamicDarkColorValue -> {
val colorScheme = ColorMappings.dynamicDarkColorScheme(context, accentColor)
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = colorScheme.getColor(value.colorName))
}
is SnyggGenericFontFamilyValue, is SnyggCustomFontFamilyValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FontDownload,
contentDescription = null,
)
}
is SnyggFontStyleValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FormatItalic,
contentDescription = null,
)
}
is SnyggFontWeightValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FormatBold,
contentDescription = null,
)
}
is SnyggPaddingValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.Padding,
contentDescription = null,
)
is SnyggMaterialYouValue -> {
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = value.loadColor(LocalContext.current))
}
is SnyggShapeValue -> {
@@ -175,7 +102,6 @@ internal fun SnyggValueIcon(
.border(spec.borderWith, MaterialTheme.colorScheme.onBackground, value.alwaysPercentShape())
)
}
is SnyggDpSizeValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
@@ -190,37 +116,6 @@ internal fun SnyggValueIcon(
contentDescription = null,
)
}
is SnyggTextAlignValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = when (value.textAlign) {
TextAlign.Left, TextAlign.Start -> Icons.AutoMirrored.Default.FormatAlignLeft
TextAlign.Right, TextAlign.End -> Icons.AutoMirrored.Default.FormatAlignRight
TextAlign.Justify -> Icons.Default.FormatAlignJustify
else -> Icons.Default.FormatAlignCenter
},
contentDescription = null,
)
}
is SnyggTextDecorationLineValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = when (value.textDecoration) {
TextDecoration.LineThrough -> Icons.Default.FormatStrikethrough
else -> Icons.Default.FormatUnderlined
},
contentDescription = null,
)
}
is SnyggTextOverflowValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.AutoMirrored.Default.WrapText,
contentDescription = null,
)
}
is SnyggDefinedVarValue -> {
val realValue = definedVariables[value.key]
if (realValue == null) {
@@ -255,37 +150,6 @@ internal fun SnyggValueIcon(
}
}
}
is SnyggUriValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.AttachFile,
contentDescription = null,
)
}
is SnyggContentScaleValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.OpenInFull,
contentDescription = null,
)
}
is SnyggYesValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FormatBold,
contentDescription = null,
)
}
is SnyggNoValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.CheckBoxOutlineBlank,
contentDescription = null,
)
}
else -> {
// Render nothing
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,16 +36,11 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
@@ -58,8 +53,11 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -73,18 +71,17 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.apptheme.Shapes
import dev.patrickgold.florisboard.app.ext.ExtensionComponentView
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.ime.theme.FlorisImeUiSpec
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentEditor
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionEditor
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.extPreviewTheme
import dev.patrickgold.florisboard.lib.cache.CacheManager
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.PreviewKeyboardField
import dev.patrickgold.florisboard.lib.compose.Validation
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
import dev.patrickgold.florisboard.lib.compose.rememberPreviewFieldController
@@ -96,46 +93,26 @@ import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.kotlin.io.readJson
import org.florisboard.lib.kotlin.io.subFile
import org.florisboard.lib.snygg.SnyggAnnotationRule
import org.florisboard.lib.snygg.SnyggElementRule
import org.florisboard.lib.snygg.SnyggJsonConfiguration
import org.florisboard.lib.snygg.SnyggMultiplePropertySetsEditor
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.snygg.SnyggPropertySetEditor
import org.florisboard.lib.snygg.SnyggPropertySetSpec
import org.florisboard.lib.snygg.SnyggRule
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.SnyggSinglePropertySetEditor
import org.florisboard.lib.snygg.SnyggSpec
import org.florisboard.lib.snygg.SnyggSpecDecl
import org.florisboard.lib.snygg.SnyggStylesheet
import org.florisboard.lib.snygg.SnyggStylesheetEditor
import org.florisboard.lib.snygg.ui.Saver
import kotlin.Boolean
import kotlin.String
import org.florisboard.lib.snygg.SnyggStylesheetJsonConfig
import org.florisboard.lib.snygg.definedVariablesRule
import org.florisboard.lib.snygg.isDefinedVariablesRule
internal val PrettyPrintConfig = SnyggJsonConfiguration.of(
prettyPrint = true,
prettyPrintIndent = " ",
internal val IntListSaver = Saver<SnapshotStateList<Int>, ArrayList<Int>>(
save = { ArrayList(it) },
restore = { it.toMutableStateList() },
)
private val LenientConfig = SnyggJsonConfiguration.of(
ignoreMissingSchema = true,
ignoreInvalidSchema = true,
ignoreUnsupportedSchema = true,
ignoreInvalidRules = true,
ignoreInvalidProperties = true,
ignoreInvalidValues = true,
)
private enum class StylesheetLoadingStrategy {
TRY_LOAD_OR_ASK_ON_CONFLICT, // default state
TRY_LOAD_OR_EMPTY, // user chose to not auto-fix errors
TRY_LOAD_OR_PARSE_LENIENT; // user chose to auto-fix errors
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ThemeEditorScreen(
@@ -153,66 +130,35 @@ fun ThemeEditorScreen(
val scope = rememberCoroutineScope()
val previewFieldController = rememberPreviewFieldController().also { it.isVisible = true }
var stylesheetLoadingStrategy by rememberSaveable {
mutableStateOf(StylesheetLoadingStrategy.TRY_LOAD_OR_ASK_ON_CONFLICT)
}
var stylesheetEditorFailure by remember { mutableStateOf<Throwable?>(null) }
val stylesheetEditor = remember(stylesheetLoadingStrategy) {
val stylesheetEditor = remember {
editor.stylesheetEditor ?: run {
stylesheetEditorFailure = null
val stylesheetPath = editor.stylesheetPath()
editor.stylesheetPathOnLoad = stylesheetPath
val stylesheetFile = workspace.extDir.subFile(stylesheetPath)
val stylesheetEditor = if (stylesheetFile.exists()) {
try {
val stylesheetJson = stylesheetFile.readText()
val config = when (stylesheetLoadingStrategy) {
StylesheetLoadingStrategy.TRY_LOAD_OR_PARSE_LENIENT -> LenientConfig
else -> PrettyPrintConfig
}
SnyggStylesheet.fromJson(stylesheetJson, config).getOrThrow().edit(CustomRuleComparator)
} catch (error: Throwable) {
stylesheetEditorFailure = when (stylesheetLoadingStrategy) {
StylesheetLoadingStrategy.TRY_LOAD_OR_ASK_ON_CONFLICT -> error
else -> null
}
SnyggStylesheetEditor(SnyggStylesheet.SCHEMA_V2, comparator = CustomRuleComparator)
stylesheetFile.readJson<SnyggStylesheet>(SnyggStylesheetJsonConfig).edit()
} catch (e: Throwable) {
SnyggStylesheetEditor()
}
} else {
SnyggStylesheetEditor(SnyggStylesheet.SCHEMA_V2, comparator = CustomRuleComparator)
SnyggStylesheetEditor()
}
if (stylesheetEditor.rules.none { (rule, _) -> rule.isDefinedVariablesRule() }) {
stylesheetEditor.rules[SnyggRule.definedVariablesRule()] = SnyggPropertySetEditor()
}
stylesheetEditor.rules.putIfAbsent(SnyggAnnotationRule.Defines, SnyggSinglePropertySetEditor())
stylesheetEditor
}.also { editor.stylesheetEditor = it }
}
val definedVariables = remember(stylesheetEditor.rules) {
stylesheetEditor.rules.firstNotNullOfOrNull { (rule, propertySet) ->
if (rule is SnyggAnnotationRule.Defines && propertySet is SnyggSinglePropertySetEditor) {
propertySet.properties
} else {
null
}
} ?: emptyMap()
}
val fontNames = remember(stylesheetEditor.rules) {
stylesheetEditor.rules.mapNotNull { (rule, _) ->
if (rule is SnyggAnnotationRule.Font) {
rule.fontName
} else {
null
}
}
}
val snyggLevel by prefs.theme.editorLevel.observeAsState()
val colorRepresentation by prefs.theme.editorColorRepresentation.observeAsState()
val displayColorsAs by prefs.theme.editorDisplayColorsAs.observeAsState()
val displayKbdAfterDialogs by prefs.theme.editorDisplayKbdAfterDialogs.observeAsState()
var oldFocusState by remember { mutableStateOf(false) }
var snyggRuleToEdit by rememberSaveable(stateSaver = SnyggRule.Saver) { mutableStateOf(null) }
var snyggPropertyToEdit by remember { mutableStateOf<PropertyInfo?>(null) }
var snyggPropertySetForEditing = remember<SnyggSinglePropertySetEditor?> { null }
var snyggPropertySetForEditing = remember<SnyggPropertySetEditor?> { null }
var snyggPropertySetSpecForEditing = remember<SnyggPropertySetSpec?> { null }
var showEditComponentMetaDialog by rememberSaveable { mutableStateOf(false) }
var showFineTuneDialog by rememberSaveable { mutableStateOf(false) }
@@ -252,33 +198,6 @@ fun ThemeEditorScreen(
}
content {
stylesheetEditorFailure?.let { failure ->
JetPrefAlertDialog(
title = stringRes(R.string.settings__theme_editor__stylesheet_error_title),
confirmLabel = stringRes(R.string.action__yes),
onConfirm = {
editor.stylesheetEditor = null
stylesheetLoadingStrategy = StylesheetLoadingStrategy.TRY_LOAD_OR_PARSE_LENIENT
},
dismissLabel = stringRes(R.string.action__no),
onDismiss = {
editor.stylesheetEditor = null
stylesheetLoadingStrategy = StylesheetLoadingStrategy.TRY_LOAD_OR_EMPTY
},
) {
Column {
Text(
modifier = Modifier.padding(bottom = 8.dp),
text = failure.message.toString(),
fontStyle = FontStyle.Italic,
)
Text(
text = stringRes(R.string.settings__theme_editor__stylesheet_error_description),
)
}
}
}
BackHandler {
handleBackPress()
}
@@ -310,15 +229,23 @@ fun ThemeEditorScreen(
DisposableEffect(workspace.version) {
themeManager.previewThemeInfo = ThemeManager.ThemeInfo.DEFAULT.copy(
name = extPreviewTheme(System.currentTimeMillis().toString()),
stylesheet = stylesheetEditor.build(),
loadedDir = workspace.extDir,
stylesheet = stylesheetEditor.build().compileToFullyQualified(FlorisImeUiSpec),
)
onDispose {
themeManager.previewThemeInfo = null
}
}
val definedVariables = remember(stylesheetEditor.rules) {
stylesheetEditor.rules.firstNotNullOfOrNull { (rule, propertySet) ->
if (rule.isDefinedVariablesRule()) {
propertySet.properties
} else {
null
}
} ?: emptyMap()
}
// TODO: (priority = low)
// Floris scrollbar does not like lazy lists with non-constant item heights.
// Consider building a custom scrollbar tailored for this list specifically.
@@ -336,7 +263,7 @@ fun ThemeEditorScreen(
onEditBtnClick = { showEditComponentMetaDialog = true },
)
if (stylesheetEditor.rules.isEmpty() ||
(stylesheetEditor.rules.size == 1 && stylesheetEditor.rules.all { (rule, _) -> rule == SnyggAnnotationRule.Defines })
(stylesheetEditor.rules.size == 1 && stylesheetEditor.rules.keys.all { it.isDefinedVariablesRule() })
) {
Text(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
@@ -347,9 +274,9 @@ fun ThemeEditorScreen(
}
}
items(stylesheetEditor.rules.toList()) { (rule, propertySet) -> key(rule) {
val propertySetSpec = SnyggSpec.propertySetSpecOf(rule)
val isVariablesRule = rule == SnyggAnnotationRule.Defines
items(stylesheetEditor.rules.entries.toList()) { (rule, propertySet) -> key(rule) {
val isVariablesRule = rule.isDefinedVariablesRule()
val propertySetSpec = FlorisImeUiSpec.propertySetSpec(rule.element)
FlorisOutlinedBox(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
@@ -364,129 +291,33 @@ fun ThemeEditorScreen(
snyggRuleToEdit = rule
},
onAddPropertyBtnClick = {
when(propertySet) {
is SnyggMultiplePropertySetsEditor -> {
workspace.update {
propertySet.sets.add(SnyggSinglePropertySetEditor())
}
}
is SnyggSinglePropertySetEditor -> {
snyggPropertySetForEditing = propertySet
snyggPropertyToEdit = SnyggEmptyPropertyInfoForAdding.copy(
rule = rule,
)
}
}
snyggPropertySetForEditing = propertySet
snyggPropertySetSpecForEditing = propertySetSpec
snyggPropertyToEdit = SnyggEmptyPropertyInfoForAdding
},
)
if (isVariablesRule) {
Text(
modifier = Modifier.padding(bottom = 8.dp, start = 16.dp, end = 16.dp),
text = stringRes(R.string.snygg__rule_annotation__defines_description),
text = stringRes(R.string.snygg__rule_element__defines_description),
style = MaterialTheme.typography.bodyMedium,
fontStyle = FontStyle.Italic,
)
}
@Composable
fun SinglePropertySetEditor(
propertySet: SnyggSinglePropertySetEditor,
) {
for ((propertyName, propertySpec) in propertySetSpec?.properties.orEmpty()) {
if (propertySpec.required && !propertySet.properties.containsKey(propertyName)) {
FlorisOutlinedBox(title = "Errors", modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)) {
Text(
modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp),
text = "Required property '$propertyName' does not exist",
color = MaterialTheme.colorScheme.error,
)
}
}
}
for ((propertyName, propertyValue) in propertySet.properties) {
if (true /*propertySpec != null && propertySpec.level <= snyggLevel*/ || isVariablesRule) {
JetPrefListItem(
modifier = Modifier.rippleClickable {
snyggPropertySetForEditing = propertySet
snyggPropertyToEdit = PropertyInfo(rule, propertyName, propertyValue)
},
text = context.translatePropertyName(propertyName, snyggLevel),
secondaryText = context.translatePropertyValue(propertyValue, snyggLevel, colorRepresentation),
singleLineSecondaryText = true,
trailing = { SnyggValueIcon(propertyValue, definedVariables) },
)
}
}
}
when (propertySet) {
is SnyggSinglePropertySetEditor -> {
SinglePropertySetEditor(propertySet)
}
is SnyggMultiplePropertySetsEditor -> {
val sets = propertySet.sets
sets.forEachIndexed { propertySetIndex, propertySet ->
key(propertySet.uuid) {
FlorisOutlinedBox(Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)) {
Row {
Text("Source set", Modifier
.padding(start = 16.dp)
.align(Alignment.CenterVertically))
Spacer(Modifier.weight(1f))
FlorisIconButton(
onClick = {
workspace.update {
if (propertySetIndex > 0) {
val set = sets.removeAt(propertySetIndex)
sets.add(propertySetIndex - 1, set)
}
}
},
icon = Icons.Default.KeyboardArrowUp,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
enabled = propertySetIndex > 0,
)
FlorisIconButton(
onClick = {
workspace.update {
if (propertySetIndex + 1 < sets.size) {
val set = sets.removeAt(propertySetIndex)
sets.add(propertySetIndex + 1, set)
}
}
},
icon = Icons.Default.KeyboardArrowDown,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
enabled = propertySetIndex + 1 < sets.size,
)
FlorisIconButton(
onClick = {
workspace.update {
sets.removeAt(propertySetIndex)
}
},
icon = Icons.Default.Delete,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
)
FlorisIconButton(
onClick = {
snyggPropertySetForEditing = propertySet
snyggPropertyToEdit = SnyggEmptyPropertyInfoForAdding.copy(
rule = rule,
)
},
icon = Icons.Default.Add,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
)
}
SinglePropertySetEditor(propertySet)
}
}
}
for ((propertyName, propertyValue) in propertySet.properties) {
val propertySpec = propertySetSpec?.propertySpec(propertyName)
if (propertySpec != null && propertySpec.level <= snyggLevel || isVariablesRule) {
JetPrefListItem(
modifier = Modifier.rippleClickable {
snyggPropertySetForEditing = propertySet
snyggPropertySetSpecForEditing = propertySetSpec
snyggPropertyToEdit = PropertyInfo(propertyName, propertyValue)
},
text = translatePropertyName(propertyName, snyggLevel),
secondaryText = translatePropertyValue(propertyValue, snyggLevel, displayColorsAs),
singleLineSecondaryText = true,
trailing = { SnyggValueIcon(propertyValue, definedVariables) },
)
}
}
}
@@ -538,14 +369,7 @@ fun ThemeEditorScreen(
true
}
oldRule == SnyggEmptyRuleForAdding -> {
when (SnyggSpec.propertySetSpecOf(newRule)!!.type) {
SnyggSpecDecl.PropertySet.Type.SINGLE_SET -> {
rules[newRule] = SnyggSinglePropertySetEditor()
}
SnyggSpecDecl.PropertySet.Type.MULTIPLE_SETS -> {
rules[newRule] = SnyggMultiplePropertySetsEditor()
}
}
rules[newRule] = SnyggPropertySetEditor()
snyggRuleToEdit = null
scope.launch {
lazyListState.animateScrollToItem(index = rules.keys.indexOf(newRule))
@@ -572,12 +396,11 @@ fun ThemeEditorScreen(
val propertyToEdit = snyggPropertyToEdit
if (propertyToEdit != null) {
EditPropertyDialog(
propertySetSpec = snyggPropertySetSpecForEditing,
initProperty = propertyToEdit,
level = snyggLevel,
colorRepresentation = colorRepresentation,
displayColorsAs = displayColorsAs,
definedVariables = definedVariables,
fontNames = fontNames,
workspace = workspace,
onConfirmNewValue = { name, value ->
val properties = snyggPropertySetForEditing?.properties ?: return@EditPropertyDialog false
if (propertyToEdit == SnyggEmptyPropertyInfoForAdding && properties.containsKey(name)) {
@@ -618,6 +441,8 @@ private fun ComponentMetaEditorDialog(
var authors by rememberSaveable { mutableStateOf(editor.authors.joinToString("\n")) }
val authorsValidation = rememberValidationResult(ExtensionValidation.ComponentAuthors, authors)
var isNightTheme by rememberSaveable { mutableStateOf(editor.isNightTheme) }
var isBorderless by rememberSaveable { mutableStateOf(editor.isBorderless) }
val isMaterialYouAware by rememberSaveable { mutableStateOf(editor.isMaterialYouAware) }
var stylesheetPath by rememberSaveable { mutableStateOf(editor.stylesheetPath) }
val stylesheetPathValidation = rememberValidationResult(ExtensionValidation.ThemeComponentStylesheetPath, stylesheetPath)
@@ -639,6 +464,8 @@ private fun ComponentMetaEditorDialog(
editor.label = label.trim()
editor.authors = authors.lines().map { it.trim() }.filter { it.isNotBlank() }
editor.isNightTheme = isNightTheme
editor.isBorderless = isBorderless
editor.isMaterialYouAware = isMaterialYouAware
editor.stylesheetPath = stylesheetPath.trim()
}
onConfirm()
@@ -650,28 +477,31 @@ private fun ComponentMetaEditorDialog(
) {
Column {
DialogProperty(text = stringRes(R.string.ext__meta__id)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = id,
onValueChange = { id = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
singleLine = true,
showValidationError = showValidationErrors,
validationResult = idValidation,
)
Validation(showValidationErrors, idValidation)
}
DialogProperty(text = stringRes(R.string.ext__meta__label)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = label,
onValueChange = { label = it },
singleLine = true,
showValidationError = showValidationErrors,
validationResult = labelValidation,
)
Validation(showValidationErrors, labelValidation)
}
DialogProperty(text = stringRes(R.string.ext__meta__authors)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = authors,
onValueChange = { authors = it },
showValidationError = showValidationErrors,
validationResult = authorsValidation,
)
Validation(showValidationErrors, authorsValidation)
}
JetPrefListItem(
modifier = Modifier.toggleable(isNightTheme) { isNightTheme = it },
@@ -679,21 +509,28 @@ private fun ComponentMetaEditorDialog(
trailing = {
Switch(checked = isNightTheme, onCheckedChange = null)
},
colors = ListItemDefaults.colors(containerColor = AlertDialogDefaults.containerColor)
)
JetPrefListItem(
modifier = Modifier.toggleable(isBorderless) { isBorderless = it },
text = stringRes(R.string.settings__theme_editor__component_meta_is_borderless),
trailing = {
Switch(checked = isBorderless, onCheckedChange = null)
},
)
DialogProperty(text = stringRes(R.string.settings__theme_editor__component_meta_stylesheet_path)) {
JetPrefTextField(
FlorisOutlinedTextField(
value = stylesheetPath,
onValueChange = { stylesheetPath = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
singleLine = true,
placeholderText = if (stylesheetPath.isEmpty()) {
placeholder = if (stylesheetPath.isEmpty()) {
ThemeExtensionComponent.defaultStylesheetPath(id.trim())
} else {
null
},
showValidationError = showValidationErrors,
validationResult = stylesheetPathValidation,
)
Validation(showValidationErrors, stylesheetPathValidation)
}
}
}
@@ -707,8 +544,6 @@ private fun SnyggRuleRow(
onEditRuleBtnClick: () -> Unit,
onAddPropertyBtnClick: () -> Unit,
) {
val context = LocalContext.current
@Composable
fun Selector(text: String) {
Text(
@@ -745,53 +580,38 @@ private fun SnyggRuleRow(
.weight(1f)
.padding(vertical = 8.dp, horizontal = 10.dp),
) {
if (rule is SnyggElementRule) {
Text(
text = context.translateElementName(rule, level),
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(modifier = Modifier.fillMaxWidth()) {
if (rule.selector == SnyggSelector.PRESSED) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.PRESSED.id
else -> stringRes(R.string.snygg__rule_selector__pressed)
}
)
}
if (rule.selector == SnyggSelector.FOCUS) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.FOCUS.id
else -> stringRes(R.string.snygg__rule_selector__focus)
}
)
}
if (rule.selector == SnyggSelector.HOVER) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.HOVER.id
else -> stringRes(R.string.snygg__rule_selector__hover)
}
)
}
if (rule.selector == SnyggSelector.DISABLED) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.DISABLED.id
else -> stringRes(R.string.snygg__rule_selector__disabled)
}
)
}
Text(
text = translateElementName(rule, level),
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(modifier = Modifier.fillMaxWidth()) {
if (rule.pressedSelector) {
Selector(text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.PRESSED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__pressed)
})
}
for ((attrKey, attrValue) in rule.attributes) {
AttributesList(text = attrKey, list = attrValue.toString())
if (rule.focusSelector) {
Selector(text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.FOCUS_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__focus)
})
}
} else {
Text(text = rule.toString())
if (rule.disabledSelector) {
Selector(text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.DISABLED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__disabled)
})
}
}
if (rule.codes.isNotEmpty()) {
AttributesList(text = "code", list = remember(rule.codes) { rule.codes.toString() })
}
if (rule.shiftStates.isNotEmpty()) {
AttributesList(text = "shiftstate", list = remember(rule.shiftStates) { rule.shiftStates.toString() })
}
}
if (showEditBtn) {
@@ -825,31 +645,10 @@ internal fun DialogProperty(
.weight(1f)
.padding(vertical = 8.dp),
text = text,
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleSmall,
)
trailingIconTitle()
}
content()
}
}
private object CustomRuleComparator : Comparator<SnyggRule> {
@Suppress("IfThenToElvis")
override fun compare(a: SnyggRule, b: SnyggRule): Int {
return if (a !is SnyggElementRule || b !is SnyggElementRule || a.elementName == b.elementName) {
a.compareTo(b)
} else {
val aOrdinal = FlorisImeUi.elementNamesToOrdinals[a.elementName]
val bOrdinal = FlorisImeUi.elementNamesToOrdinals[b.elementName]
if (aOrdinal == null && bOrdinal == null) {
a.elementName.compareTo(b.elementName)
} else if (bOrdinal == null) {
-1
} else if (aOrdinal == null) {
1
} else {
aOrdinal.compareTo(bOrdinal)
}
}
}
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,14 +19,12 @@ 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
@@ -43,11 +41,8 @@ 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 {
@@ -113,22 +108,6 @@ 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-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,108 +16,134 @@
package dev.patrickgold.florisboard.app.settings.theme
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.lib.UnicodeCtrlChar
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
import org.florisboard.lib.kotlin.simpleNameOrEnclosing
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.Snygg
import org.florisboard.lib.snygg.SnyggElementRule
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.snygg.SnyggRule
import org.florisboard.lib.snygg.value.RgbaColor
import org.florisboard.lib.snygg.value.SnyggCircleShapeValue
import org.florisboard.lib.snygg.value.SnyggCustomFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggCutCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggCutCornerPercentShapeValue
import org.florisboard.lib.snygg.value.SnyggDefinedVarValue
import org.florisboard.lib.snygg.value.SnyggDpSizeValue
import org.florisboard.lib.snygg.value.SnyggDynamicDarkColorValue
import org.florisboard.lib.snygg.value.SnyggDynamicLightColorValue
import org.florisboard.lib.snygg.value.SnyggFontStyleValue
import org.florisboard.lib.snygg.value.SnyggFontWeightValue
import org.florisboard.lib.snygg.value.SnyggGenericFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggInheritValue
import org.florisboard.lib.snygg.value.SnyggNoValue
import org.florisboard.lib.snygg.value.SnyggContentScaleValue
import org.florisboard.lib.snygg.value.SnyggPaddingValue
import org.florisboard.lib.snygg.value.SnyggExplicitInheritValue
import org.florisboard.lib.snygg.value.SnyggImplicitInheritValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouDarkColorValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouLightColorValue
import org.florisboard.lib.snygg.value.SnyggPercentageSizeValue
import org.florisboard.lib.snygg.value.SnyggRectangleShapeValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerPercentShapeValue
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
import org.florisboard.lib.snygg.value.SnyggSpSizeValue
import org.florisboard.lib.snygg.value.SnyggStaticColorValue
import org.florisboard.lib.snygg.value.SnyggTextAlignValue
import org.florisboard.lib.snygg.value.SnyggTextDecorationLineValue
import org.florisboard.lib.snygg.value.SnyggTextMaxLinesValue
import org.florisboard.lib.snygg.value.SnyggTextOverflowValue
import org.florisboard.lib.snygg.value.SnyggUndefinedValue
import org.florisboard.lib.snygg.value.SnyggUriValue
import org.florisboard.lib.snygg.value.SnyggValue
import org.florisboard.lib.snygg.value.SnyggValueEncoder
import org.florisboard.lib.snygg.value.SnyggYesValue
import kotlin.math.roundToInt
internal fun Context.translateElementName(rule: SnyggElementRule, level: SnyggLevel): String {
return translateElementName(rule.elementName, level) ?: rule.elementName
}
internal fun Context.translateElementName(element: String, level: SnyggLevel): String? {
return when (level) {
SnyggLevel.DEVELOPER -> null
else -> FlorisImeUi.elementNamesToTranslation[element]?.let { getString(it) }
@Composable
internal fun translateElementName(rule: SnyggRule, level: SnyggLevel): String {
return translateElementName(rule.element, level) ?: remember {
buildString {
if (rule.isAnnotation) {
append(SnyggRule.ANNOTATION_MARKER)
}
append(rule.element)
}
}
}
private val PropertyNameMap = mapOf(
Snygg.Background to R.string.snygg__property_name__background,
Snygg.Foreground to R.string.snygg__property_name__foreground,
Snygg.BackgroundImage to R.string.snygg__property_name__background_image,
Snygg.ContentScale to R.string.snygg__property_name__content_scale,
Snygg.BorderColor to R.string.snygg__property_name__border_color,
Snygg.BorderStyle to R.string.snygg__property_name__border_style,
Snygg.BorderWidth to R.string.snygg__property_name__border_width,
Snygg.FontFamily to R.string.snygg__property_name__font_family,
Snygg.FontSize to R.string.snygg__property_name__font_size,
Snygg.FontStyle to R.string.snygg__property_name__font_style,
Snygg.FontWeight to R.string.snygg__property_name__font_weight,
Snygg.LetterSpacing to R.string.snygg__property_name__letter_spacing,
Snygg.LineHeight to R.string.snygg__property_name__line_height,
Snygg.Margin to R.string.snygg__property_name__margin,
Snygg.Padding to R.string.snygg__property_name__padding,
Snygg.ShadowColor to R.string.snygg__property_name__shadow_color,
Snygg.ShadowElevation to R.string.snygg__property_name__shadow_elevation,
Snygg.Shape to R.string.snygg__property_name__shape,
Snygg.Clip to R.string.snygg__property_name__clip,
Snygg.Src to R.string.snygg__property_name__src,
Snygg.TextAlign to R.string.snygg__property_name__text_align,
Snygg.TextDecorationLine to R.string.snygg__property_name__text_decoration_line,
Snygg.TextMaxLines to R.string.snygg__property_name__text_max_lines,
Snygg.TextOverflow to R.string.snygg__property_name__text_overflow,
"--primary" to R.string.snygg__property_name__var_primary,
"--primary-variant" to R.string.snygg__property_name__var_primary_variant,
"--secondary" to R.string.snygg__property_name__var_secondary,
"--secondary-variant" to R.string.snygg__property_name__var_secondary_variant,
"--background" to R.string.snygg__property_name__var_background,
"--surface" to R.string.snygg__property_name__var_surface,
"--surface-variant" to R.string.snygg__property_name__var_surface_variant,
"--on-primary" to R.string.snygg__property_name__var_on_primary,
"--on-secondary" to R.string.snygg__property_name__var_on_secondary,
"--on-background" to R.string.snygg__property_name__var_on_background,
"--on-surface" to R.string.snygg__property_name__var_on_surface,
"--on-surface-variant" to R.string.snygg__property_name__var_on_surface_variant,
"--shape" to R.string.snygg__property_name__var_shape,
"--shape-variant" to R.string.snygg__property_name__var_shape_variant
)
internal fun Context.translatePropertyName(propertyName: String, level: SnyggLevel): String {
@Composable
internal fun translateElementName(element: String, level: SnyggLevel): String? {
return when (level) {
SnyggLevel.DEVELOPER -> null
else -> PropertyNameMap[propertyName]
else -> when (element) {
"defines" -> R.string.snygg__rule_element__defines
FlorisImeUi.Keyboard -> R.string.snygg__rule_element__keyboard
FlorisImeUi.Key -> R.string.snygg__rule_element__key
FlorisImeUi.KeyHint -> R.string.snygg__rule_element__key_hint
FlorisImeUi.KeyPopup -> R.string.snygg__rule_element__key_popup
FlorisImeUi.ClipboardHeader -> R.string.snygg__rule_element__clipboard_header
FlorisImeUi.ClipboardItem -> R.string.snygg__rule_element__clipboard_item
FlorisImeUi.ClipboardItemPopup -> R.string.snygg__rule_element__clipboard_item_popup
FlorisImeUi.EmojiKey -> R.string.snygg__rule_element__emoji_key
FlorisImeUi.EmojiKeyPopup -> R.string.snygg__rule_element__emoji_key_popup
FlorisImeUi.EmojiTab -> R.string.snygg__rule_element__emoji_key_tab
FlorisImeUi.ExtractedLandscapeInputLayout -> R.string.snygg__rule_element__extracted_landscape_input_layout
FlorisImeUi.ExtractedLandscapeInputField -> R.string.snygg__rule_element__extracted_landscape_input_field
FlorisImeUi.ExtractedLandscapeInputAction -> R.string.snygg__rule_element__extracted_landscape_input_action
FlorisImeUi.GlideTrail -> R.string.snygg__rule_element__glide_trail
FlorisImeUi.IncognitoModeIndicator -> R.string.snygg__rule_element__incognito_mode_indicator
FlorisImeUi.OneHandedPanel -> R.string.snygg__rule_element__one_handed_panel
FlorisImeUi.Smartbar -> R.string.snygg__rule_element__smartbar
FlorisImeUi.SmartbarSharedActionsRow -> R.string.snygg__rule_element__smartbar_shared_actions_row
FlorisImeUi.SmartbarSharedActionsToggle -> R.string.snygg__rule_element__smartbar_shared_actions_toggle
FlorisImeUi.SmartbarExtendedActionsRow -> R.string.snygg__rule_element__smartbar_extended_actions_row
FlorisImeUi.SmartbarExtendedActionsToggle -> R.string.snygg__rule_element__smartbar_extended_actions_toggle
FlorisImeUi.SmartbarActionKey -> R.string.snygg__rule_element__smartbar_action_key
FlorisImeUi.SmartbarActionTile -> R.string.snygg__rule_element__smartbar_action_tile
FlorisImeUi.SmartbarActionsOverflow -> R.string.snygg__rule_element__smartbar_actions_overflow
FlorisImeUi.SmartbarActionsOverflowCustomizeButton -> R.string.snygg__rule_element__smartbar_actions_overflow_customize_button
FlorisImeUi.SmartbarActionsEditor -> R.string.snygg__rule_element__smartbar_actions_editor
FlorisImeUi.SmartbarActionsEditorHeader -> R.string.snygg__rule_element__smartbar_actions_editor_header
FlorisImeUi.SmartbarActionsEditorSubheader -> R.string.snygg__rule_element__smartbar_actions_editor_subheader
FlorisImeUi.SmartbarCandidatesRow -> R.string.snygg__rule_element__smartbar_candidates_row
FlorisImeUi.SmartbarCandidateWord -> R.string.snygg__rule_element__smartbar_candidate_word
FlorisImeUi.SmartbarCandidateClip -> R.string.snygg__rule_element__smartbar_candidate_clip
FlorisImeUi.SmartbarCandidateSpacer -> R.string.snygg__rule_element__smartbar_candidate_spacer
FlorisImeUi.SystemNavBar -> R.string.snygg__rule_element__system_nav_bar
else -> null
}
}.let { if (it != null) { stringRes(it) } else { null } }
}
@Composable
internal fun translatePropertyName(propertyName: String, level: SnyggLevel): String {
return when (level) {
SnyggLevel.DEVELOPER -> null
else -> when (propertyName) {
Snygg.Width -> R.string.snygg__property_name__width
Snygg.Height -> R.string.snygg__property_name__height
Snygg.Background -> R.string.snygg__property_name__background
Snygg.Foreground -> R.string.snygg__property_name__foreground
Snygg.BorderColor -> R.string.snygg__property_name__border_color
Snygg.BorderStyle -> R.string.snygg__property_name__border_style
Snygg.BorderWidth -> R.string.snygg__property_name__border_width
Snygg.FontFamily -> R.string.snygg__property_name__font_family
Snygg.FontSize -> R.string.snygg__property_name__font_size
Snygg.FontStyle -> R.string.snygg__property_name__font_style
Snygg.FontVariant -> R.string.snygg__property_name__font_variant
Snygg.FontWeight -> R.string.snygg__property_name__font_weight
Snygg.ShadowElevation -> R.string.snygg__property_name__shadow_elevation
Snygg.Shape -> R.string.snygg__property_name__shape
"--primary" -> R.string.snygg__property_name__var_primary
"--primary-variant" -> R.string.snygg__property_name__var_primary_variant
"--secondary" -> R.string.snygg__property_name__var_secondary
"--secondary-variant" -> R.string.snygg__property_name__var_secondary_variant
"--background" -> R.string.snygg__property_name__var_background
"--surface" -> R.string.snygg__property_name__var_surface
"--surface-variant" -> R.string.snygg__property_name__var_surface_variant
"--on-primary" -> R.string.snygg__property_name__var_on_primary
"--on-secondary" -> R.string.snygg__property_name__var_on_secondary
"--on-background" -> R.string.snygg__property_name__var_on_background
"--on-surface" -> R.string.snygg__property_name__var_on_surface
"--on-surface-variant" -> R.string.snygg__property_name__var_on_surface_variant
"--shape" -> R.string.snygg__property_name__var_shape
"--shape-variant" -> R.string.snygg__property_name__var_shape_variant
else -> null
}
}.let { resId ->
when {
resId != null -> {
getString(resId)
stringRes(resId)
}
propertyName.isBlank() -> {
getString(R.string.general__select_dropdown_value_placeholder)
stringRes(R.string.general__select_dropdown_value_placeholder)
}
else -> {
propertyName
@@ -126,14 +152,15 @@ internal fun Context.translatePropertyName(propertyName: String, level: SnyggLev
}
}
internal fun Context.translatePropertyValue(
@Composable
internal fun translatePropertyValue(
propertyValue: SnyggValue,
level: SnyggLevel,
colorRepresentation: ColorRepresentation,
displayColorsAs: DisplayColorsAs,
): String {
return when (propertyValue) {
is SnyggStaticColorValue -> {
colorRepresentation.formatColor(propertyValue.color, withAlpha = true)
is SnyggSolidColorValue -> remember(propertyValue.color, displayColorsAs) {
buildColorString(propertyValue.color, displayColorsAs)
}
else -> when (level) {
SnyggLevel.DEVELOPER -> null
@@ -149,38 +176,51 @@ internal fun Context.translatePropertyValue(
}
}
private val PropertyValueEncoderNameMap = mapOf(
SnyggUndefinedValue to R.string.general__select_dropdown_value_placeholder,
SnyggInheritValue to R.string.snygg__property_value__explicit_inherit,
SnyggDefinedVarValue to R.string.snygg__property_value__defined_var,
SnyggYesValue to R.string.snygg__property_value__yes,
SnyggNoValue to R.string.snygg__property_value__no,
SnyggStaticColorValue to R.string.snygg__property_value__solid_color,
SnyggDynamicLightColorValue to R.string.snygg__property_value__material_you_light_color,
SnyggDynamicDarkColorValue to R.string.snygg__property_value__material_you_dark_color,
SnyggGenericFontFamilyValue to R.string.snygg__property_value__font_family_generic,
SnyggCustomFontFamilyValue to R.string.snygg__property_value__font_family_custom,
SnyggFontStyleValue to R.string.snygg__property_value__font_style,
SnyggFontWeightValue to R.string.snygg__property_value__font_weight,
SnyggPaddingValue to R.string.snygg__property_value__padding,
SnyggRectangleShapeValue to R.string.snygg__property_value__rectangle_shape,
SnyggCircleShapeValue to R.string.snygg__property_value__circle_shape,
SnyggCutCornerDpShapeValue to R.string.snygg__property_value__cut_corner_shape_dp,
SnyggCutCornerPercentShapeValue to R.string.snygg__property_value__cut_corner_shape_percent,
SnyggRoundedCornerDpShapeValue to R.string.snygg__property_value__rounded_corner_shape_dp,
SnyggRoundedCornerPercentShapeValue to R.string.snygg__property_value__rounded_corner_shape_percent,
SnyggDpSizeValue to R.string.snygg__property_value__dp_size,
SnyggSpSizeValue to R.string.snygg__property_value__sp_size,
SnyggPercentageSizeValue to R.string.snygg__property_value__percentage_size,
SnyggContentScaleValue to R.string.snygg__property_value__content_scale,
SnyggTextAlignValue to R.string.snygg__property_value__text_align,
SnyggTextDecorationLineValue to R.string.snygg__property_value__text_decoration_line,
SnyggTextMaxLinesValue to R.string.snygg__property_value__text_max_lines,
SnyggTextOverflowValue to R.string.snygg__property_value__text_overflow,
SnyggUriValue to R.string.snygg__property_value__uri,
)
internal fun Context.translatePropertyValueEncoderName(encoder: SnyggValueEncoder): String {
return PropertyValueEncoderNameMap[encoder]?.let { getString(it) }
?: encoder::class.simpleNameOrEnclosing().orEmpty()
internal fun buildColorString(color: Color, displayColorsAs: DisplayColorsAs): String {
return when (displayColorsAs) {
DisplayColorsAs.HEX8 -> buildString {
append(UnicodeCtrlChar.LeftToRightIsolate)
append("#")
append((color.red * RgbaColor.RedMax).roundToInt().toString(16).padStart(2, '0'))
append((color.green * RgbaColor.GreenMax).roundToInt().toString(16).padStart(2, '0'))
append((color.blue * RgbaColor.BlueMax).roundToInt().toString(16).padStart(2, '0'))
append((color.alpha * 0xFF).roundToInt().toString(16).padStart(2, '0'))
append(UnicodeCtrlChar.PopDirectionalIsolate)
}
DisplayColorsAs.RGBA -> buildString {
append(UnicodeCtrlChar.LeftToRightIsolate)
append("rgba(")
append((color.red * RgbaColor.RedMax).roundToInt())
append(",")
append((color.green * RgbaColor.GreenMax).roundToInt())
append(",")
append((color.blue * RgbaColor.BlueMax).roundToInt())
append(",")
append(color.alpha)
append(")")
append(UnicodeCtrlChar.PopDirectionalIsolate)
}
}
}
@Composable
internal fun translatePropertyValueEncoderName(encoder: SnyggValueEncoder): String {
return when (encoder) {
SnyggImplicitInheritValue -> R.string.general__select_dropdown_value_placeholder
SnyggExplicitInheritValue -> R.string.snygg__property_value__explicit_inherit
SnyggSolidColorValue -> R.string.snygg__property_value__solid_color
SnyggMaterialYouLightColorValue -> R.string.snygg__property_value__material_you_light_color
SnyggMaterialYouDarkColorValue -> R.string.snygg__property_value__material_you_dark_color
SnyggRectangleShapeValue -> R.string.snygg__property_value__rectangle_shape
SnyggCircleShapeValue -> R.string.snygg__property_value__circle_shape
SnyggCutCornerDpShapeValue -> R.string.snygg__property_value__cut_corner_shape_dp
SnyggCutCornerPercentShapeValue -> R.string.snygg__property_value__cut_corner_shape_percent
SnyggRoundedCornerDpShapeValue -> R.string.snygg__property_value__rounded_corner_shape_dp
SnyggRoundedCornerPercentShapeValue -> R.string.snygg__property_value__rounded_corner_shape_percent
SnyggDpSizeValue -> R.string.snygg__property_value__dp_size
SnyggSpSizeValue -> R.string.snygg__property_value__sp_size
SnyggPercentageSizeValue -> R.string.snygg__property_value__percentage_size
SnyggDefinedVarValue -> R.string.snygg__property_value__defined_var
else -> null
}.let { if (it != null) { stringRes(it) } else { encoder::class.simpleName ?: "" } }.toString()
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,8 +35,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.ime.keyboard.IncognitoMode
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,8 +48,6 @@ 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.vectorResource
import org.florisboard.lib.android.AndroidVersion
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
@@ -60,11 +58,11 @@ fun TypingScreen() = FlorisScreen {
val navController = LocalNavController.current
content {
// This card is temporary and is therefore not using a string resource (not so temporary as we thought...)
// This card is temporary and is therefore not using a string resource
FlorisErrorCard(
modifier = Modifier.padding(8.dp),
text = """
Suggestions (except system autofill) and spell checking are not available in this release. All
Suggestions (except system autofill) and spell checking are not available in this alpha release. All
preferences in the "Corrections" group are properly implemented though.
""".trimIndent().replace('\n', ' '),
)
@@ -81,18 +79,28 @@ fun TypingScreen() = FlorisScreen {
summary = stringRes(R.string.pref__suggestion__block_possibly_offensive__summary),
enabledIf = { prefs.suggestion.enabled isEqualTo true },
)
SwitchPreference(
prefs.suggestion.clipboardContentEnabled,
title = stringRes(R.string.pref__suggestion__clipboard_content_enabled__label),
summary = stringRes(R.string.pref__suggestion__clipboard_content_enabled__summary),
enabledIf = { prefs.suggestion.enabled isEqualTo true },
)
DialogSliderPreference(
prefs.suggestion.clipboardContentTimeout,
title = stringRes(R.string.pref__suggestion__clipboard_content_timeout__label),
valueLabel = { stringRes(R.string.pref__suggestion__clipboard_content_timeout__summary, "v" to it) },
min = 30,
max = 300,
stepIncrement = 5,
enabledIf = { prefs.suggestion.enabled isEqualTo true },
visibleIf = { prefs.suggestion.clipboardContentEnabled isEqualTo true },
)
SwitchPreference(
prefs.suggestion.api30InlineSuggestionsEnabled,
title = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__label),
summary = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__summary),
visibleIf = { AndroidVersion.ATLEAST_API30_R },
)
ListPreference(
prefs.suggestion.incognitoMode,
icon = vectorResource(id = R.drawable.ic_incognito),
title = stringRes(R.string.pref__suggestion__incognito_mode__label),
entries = enumDisplayEntriesOf(IncognitoMode::class),
)
}
PreferenceGroup(title = stringRes(R.string.pref__correction__title)) {

View File

@@ -1,19 +1,3 @@
/*
* 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-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,6 +45,9 @@ 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
@@ -53,12 +56,9 @@ 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,65 +105,128 @@ private fun FlorisScreenScope.content(
hasNotificationPermission: NotificationPermissionState,
) {
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)
}
// Show screen without notification permission if the android version is below android 13.
if (AndroidVersion.ATMOST_API32_S_V2) {
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
}
)
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)
}
// 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)
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)
}
}
}
}
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)
},
)
}
}
@@ -192,60 +255,105 @@ 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 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) {
return if (AndroidVersion.ATMOST_API32_S_V2) {
listOf(
FlorisStep(
id = Steps.SelectNotification.id,
title = stringRes(R.string.setup__grant_notification_permission__title),
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)
) {
StepText(stringRes(R.string.setup__grant_notification_permission__description))
StepButton(stringRes(R.string.setup__grant_notification_permission__btn)) {
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
if (AndroidVersion.ATLEAST_API33_T) {
requestNotification.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
}
}
)
},
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) {
data object EnableIme : Steps(id = 1)
data object SelectIme : Steps(id = 2)
data object SelectNotification : Steps(id = 3)
data object FinishUp : Steps(id = 4)
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)
}
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,16 +31,20 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
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
@@ -50,7 +54,9 @@ 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.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
@@ -67,6 +73,12 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
@@ -78,12 +90,16 @@ 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
import dev.patrickgold.florisboard.lib.compose.FlorisIconButtonWithInnerPadding
import dev.patrickgold.florisboard.lib.compose.FlorisStaggeredVerticalGrid
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.autoMirrorForRtl
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.lib.util.NetworkUtils
@@ -92,14 +108,20 @@ import org.florisboard.lib.android.AndroidKeyguardManager
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.SnyggPropertySet
import org.florisboard.lib.snygg.ui.SnyggButton
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggIconButton
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggClip
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
private val ContentPadding = PaddingValues(horizontal = 4.dp)
private val ItemMargin = PaddingValues(all = 6.dp)
private val ItemPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp)
private val DescriptionPadding = PaddingValues(top = 4.dp, start = 12.dp, end = 12.dp)
private val ItemWidth = 200.dp
private val DialogWidth = 240.dp
@@ -117,79 +139,69 @@ fun ClipboardInputLayout(
val historyEnabled by prefs.clipboard.historyEnabled.observeAsState()
val history by clipboardManager.history.observeAsNonNullState()
val innerHeight = FlorisImeSizing.imeUiHeight() - FlorisImeSizing.smartbarHeight
var popupItem by remember(history) { mutableStateOf<ClipboardItem?>(null) }
var showClearAllHistory by remember { mutableStateOf(false) }
val headerStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardHeader)
val itemStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardItem)
val popupStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardItemPopup)
val enableHistoryButtonStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardEnableHistoryButton)
fun isPopupSurfaceActive() = popupItem != null || showClearAllHistory
@Composable
fun HeaderRow() {
SnyggRow(FlorisImeUi.ClipboardHeader.elementName,
Row(
modifier = Modifier
.fillMaxWidth()
.height(FlorisImeSizing.smartbarHeight),
.height(FlorisImeSizing.smartbarHeight)
.snyggBackground(context, headerStyle),
verticalAlignment = Alignment.CenterVertically,
) {
val sizeModifier = Modifier
.sizeIn(maxHeight = FlorisImeSizing.smartbarHeight)
.aspectRatio(1f)
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
FlorisIconButtonWithInnerPadding(
onClick = { keyboardManager.activeState.imeUiMode = ImeUiMode.TEXT },
modifier = sizeModifier,
) {
SnyggIcon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
)
}
SnyggText(
elementName = FlorisImeUi.ClipboardHeaderText.elementName,
icon = Icons.AutoMirrored.Filled.ArrowBack,
iconColor = headerStyle.foreground.solidColor(context),
)
Text(
modifier = Modifier.weight(1f),
text = stringRes(R.string.clipboard__header_title),
color = headerStyle.foreground.solidColor(context),
fontSize = headerStyle.fontSize.spSize(),
)
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
FlorisIconButtonWithInnerPadding(
onClick = { prefs.clipboard.historyEnabled.set(!historyEnabled) },
modifier = sizeModifier.autoMirrorForRtl(),
modifier = Modifier.autoMirrorForRtl(),
icon = if (historyEnabled) {
Icons.Default.ToggleOn
} else {
Icons.Default.ToggleOff
},
iconColor = headerStyle.foreground.solidColor(context),
enabled = !deviceLocked && !isPopupSurfaceActive(),
) {
SnyggIcon(
imageVector = if (historyEnabled) {
Icons.Default.ToggleOn
} else {
Icons.Default.ToggleOff
},
)
}
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
)
FlorisIconButtonWithInnerPadding(
onClick = { showClearAllHistory = true },
modifier = sizeModifier.autoMirrorForRtl(),
modifier = Modifier.autoMirrorForRtl(),
icon = Icons.Default.ClearAll,
iconColor = headerStyle.foreground.solidColor(context),
enabled = !deviceLocked && historyEnabled && history.all.isNotEmpty() && !isPopupSurfaceActive(),
) {
SnyggIcon(
imageVector = Icons.Default.ClearAll,
)
}
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
)
FlorisIconButtonWithInnerPadding(
onClick = {
context.showShortToast("TODO: implement inline clip item editing")
},
modifier = sizeModifier,
icon = Icons.Default.Edit,
iconColor = headerStyle.foreground.solidColor(context),
enabled = !deviceLocked && historyEnabled && !isPopupSurfaceActive(),
) {
SnyggIcon(
imageVector = Icons.Default.Edit,
)
}
)
KeyboardLikeButton(
modifier = sizeModifier,
inputEventDispatcher = keyboardManager.inputEventDispatcher,
keyData = TextKeyData.DELETE,
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
element = FlorisImeUi.ClipboardHeader,
) {
SnyggIcon(imageVector = Icons.AutoMirrored.Outlined.Backspace)
Icon(Icons.AutoMirrored.Outlined.Backspace, null)
}
}
}
@@ -198,11 +210,16 @@ fun ClipboardInputLayout(
@Composable
fun ClipItemView(
item: ClipboardItem,
style: SnyggPropertySet,
contentScrollInsteadOfClip: Boolean,
modifier: Modifier = Modifier,
) {
SnyggBox(FlorisImeUi.ClipboardItem.elementName,
modifier = modifier.fillMaxWidth(),
SnyggSurface(
modifier = modifier
.fillMaxWidth()
.padding(ItemMargin),
style = style,
clip = true,
clickAndSemanticsModifier = Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
@@ -234,9 +251,14 @@ fun ClipboardInputLayout(
contentScale = ContentScale.FillWidth,
)
} else {
SnyggText(
modifier = Modifier.fillMaxWidth(),
Text(
modifier = Modifier
.fillMaxWidth()
.padding(ItemPadding),
text = bitmap.exceptionOrNull()?.message ?: "Unknown error",
style = TextStyle(textDirection = TextDirection.Ltr),
color = Color.Red,
fontSize = style.fontSize.spSize(),
)
}
} else if (item.type == ItemType.VIDEO) {
@@ -276,20 +298,31 @@ fun ClipboardInputLayout(
tint = Color.Black,
)
} else {
SnyggText(
modifier = Modifier.fillMaxWidth(),
Text(
modifier = Modifier
.fillMaxWidth()
.padding(ItemPadding),
text = bitmap.exceptionOrNull()?.message ?: "Unknown error",
style = TextStyle(textDirection = TextDirection.Ltr),
color = Color.Red,
fontSize = style.fontSize.spSize(),
)
}
} else {
val text = item.stringRepresentation()
Column {
ClipTextItemDescription(text)
SnyggText(
ClipTextItemDescription(text, style)
Text(
modifier = Modifier
.fillMaxWidth()
.run { if (contentScrollInsteadOfClip) this.florisVerticalScroll() else this },
.run { if (contentScrollInsteadOfClip) this.florisVerticalScroll() else this }
.padding(ItemPadding),
text = item.displayText(),
style = TextStyle(textDirection = TextDirection.ContentOrLtr),
color = style.foreground.solidColor(context),
fontSize = style.fontSize.spSize(),
maxLines = if (contentScrollInsteadOfClip) Int.MAX_VALUE else 5,
overflow = TextOverflow.Ellipsis,
)
}
}
@@ -298,12 +331,15 @@ fun ClipboardInputLayout(
@Composable
fun HistoryMainView() {
SnyggBox(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
Box(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight),
) {
val historyAlpha by animateFloatAsState(targetValue = if (isPopupSurfaceActive()) 0.12f else 1f)
SnyggColumn(
Column(
modifier = Modifier
.padding(ContentPadding)
.fillMaxSize()
.alpha(historyAlpha)
.florisVerticalScroll(),
@@ -311,38 +347,42 @@ fun ClipboardInputLayout(
if (history.pinned.isNotEmpty()) {
ClipCategoryTitle(
text = stringRes(R.string.clipboard__group_pinned),
style = itemStyle,
)
FlorisStaggeredVerticalGrid(maxColumnWidth = ItemWidth) {
for (item in history.pinned) {
ClipItemView(item, contentScrollInsteadOfClip = false)
ClipItemView(item, itemStyle, contentScrollInsteadOfClip = false)
}
}
}
if (history.recent.isNotEmpty()) {
ClipCategoryTitle(
text = stringRes(R.string.clipboard__group_recent),
style = itemStyle,
)
FlorisStaggeredVerticalGrid(maxColumnWidth = ItemWidth) {
for (item in history.recent) {
ClipItemView(item, contentScrollInsteadOfClip = false)
ClipItemView(item, itemStyle, contentScrollInsteadOfClip = false)
}
}
}
if (history.other.isNotEmpty()) {
ClipCategoryTitle(
text = stringRes(R.string.clipboard__group_other),
style = itemStyle,
)
FlorisStaggeredVerticalGrid(maxColumnWidth = ItemWidth) {
for (item in history.other) {
ClipItemView(item, contentScrollInsteadOfClip = false)
ClipItemView(item, itemStyle, contentScrollInsteadOfClip = false)
}
}
}
}
if (popupItem != null) {
SnyggRow(
Row(
modifier = Modifier
.fillMaxSize()
.padding(ContentPadding)
.pointerInput(Unit) {
detectTapGestures { popupItem = null }
},
@@ -352,9 +392,17 @@ fun ClipboardInputLayout(
ClipItemView(
modifier = Modifier.widthIn(max = ItemWidth),
item = popupItem!!,
style = itemStyle,
contentScrollInsteadOfClip = true,
)
SnyggColumn(FlorisImeUi.ClipboardItemPopup.elementName) {
Column(
modifier = Modifier
.padding(ItemMargin)
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle)
.snyggClip(popupStyle),
) {
PopupAction(
iconId = R.drawable.ic_pin,
text = stringRes(if (popupItem!!.isPinned) {
@@ -362,6 +410,7 @@ fun ClipboardInputLayout(
} else {
R.string.clip__pin_item
}),
style = popupStyle,
) {
if (popupItem!!.isPinned) {
clipboardManager.unpinClip(popupItem!!)
@@ -373,6 +422,7 @@ fun ClipboardInputLayout(
PopupAction(
iconId = R.drawable.ic_delete,
text = stringRes(R.string.clip__delete_item),
style = popupStyle,
) {
clipboardManager.deleteClip(popupItem!!)
popupItem = null
@@ -380,6 +430,7 @@ fun ClipboardInputLayout(
PopupAction(
iconId = R.drawable.ic_content_paste,
text = stringRes(R.string.clip__paste_item),
style = popupStyle,
) {
clipboardManager.pasteItem(popupItem!!)
popupItem = null
@@ -388,53 +439,51 @@ fun ClipboardInputLayout(
}
}
if (showClearAllHistory) {
SnyggRow(
Row(
modifier = Modifier
.fillMaxSize()
.padding(ContentPadding)
.pointerInput(Unit) {
detectTapGestures { showClearAllHistory = false }
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround,
) {
SnyggColumn(
elementName = FlorisImeUi.ClipboardClearAllDialog.elementName,
Column(
modifier = Modifier
.width(DialogWidth)
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle)
.snyggClip(popupStyle)
.pointerInput(Unit) {
detectTapGestures { /* Do nothing */ }
},
) {
SnyggText(
elementName = FlorisImeUi.ClipboardClearAllDialogMessage.elementName,
Text(
modifier = Modifier.padding(all = 16.dp),
text = stringRes(R.string.clipboard__confirm_clear_history__message),
color = popupStyle.foreground.solidColor(context),
)
SnyggRow(FlorisImeUi.ClipboardClearAllDialogButtons.elementName) {
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
Spacer(modifier = Modifier.weight(1f))
SnyggButton(
elementName = FlorisImeUi.ClipboardClearAllDialogButton.elementName,
attributes = mapOf("action" to "no"),
FlorisTextButton(
onClick = {
showClearAllHistory = false
},
) {
SnyggText(
text = stringRes(R.string.action__no),
)
}
SnyggButton(
elementName = FlorisImeUi.ClipboardClearAllDialogButton.elementName,
attributes = mapOf("action" to "yes"),
modifier = Modifier.padding(end = 8.dp),
text = stringRes(R.string.action__no),
colors = ButtonDefaults.textButtonColors(contentColor = popupStyle.foreground.solidColor(context)),
)
FlorisTextButton(
onClick = {
clipboardManager.clearHistory()
context.showShortToast(R.string.clipboard__cleared_history)
showClearAllHistory = false
},
) {
SnyggText(
text = stringRes(R.string.action__yes),
)
}
text = stringRes(R.string.action__yes),
colors = ButtonDefaults.textButtonColors(contentColor = popupStyle.foreground.solidColor(context)),
)
}
}
}
@@ -444,62 +493,98 @@ fun ClipboardInputLayout(
@Composable
fun HistoryEmptyView() {
SnyggColumn(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
Column(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight)
.padding(ContentPadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SnyggText(
Text(
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp),
text = stringRes(R.string.clipboard__empty__title),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
fontWeight = FontWeight.Bold,
)
SnyggText(
Text(
text = stringRes(R.string.clipboard__empty__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
textAlign = TextAlign.Center,
)
}
}
@Composable
fun HistoryDisabledView() {
SnyggColumn(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
Box(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight)
.padding(ContentPadding),
contentAlignment = Alignment.TopCenter,
) {
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryDisabledTitle.elementName,
modifier = Modifier.padding(bottom = 8.dp),
text = stringRes(R.string.clipboard__disabled__title),
)
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryDisabledMessage.elementName,
text = stringRes(R.string.clipboard__disabled__message),
)
SnyggButton(FlorisImeUi.ClipboardHistoryDisabledButton.elementName,
onClick = { prefs.clipboard.historyEnabled.set(true) },
modifier = Modifier.align(Alignment.End),
SnyggSurface(
modifier = Modifier
.padding(ItemMargin)
.fillMaxWidth()
.wrapContentHeight(),
style = itemStyle,
contentPadding = ItemPadding,
) {
SnyggText(
text = stringRes(R.string.clipboard__disabled__enable_button),
)
Column(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier.padding(bottom = 8.dp),
text = stringRes(R.string.clipboard__disabled__title),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
fontWeight = FontWeight.Bold,
)
Text(
text = stringRes(R.string.clipboard__disabled__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
)
SnyggButton(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.End),
onClick = { prefs.clipboard.historyEnabled.set(true) },
style = enableHistoryButtonStyle,
text = stringRes(R.string.clipboard__disabled__enable_button)
)
}
}
}
}
@Composable
fun HistoryLockedView() {
SnyggColumn(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
Column(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight)
.padding(ContentPadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryLockedTitle.elementName,
Text(
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp),
text = stringRes(R.string.clipboard__locked__title),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
fontWeight = FontWeight.Bold,
)
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryLockedMessage.elementName,
Text(
text = stringRes(R.string.clipboard__locked__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
textAlign = TextAlign.Center,
)
}
}
SnyggColumn(
Column(
modifier = modifier
.fillMaxWidth()
.height(FlorisImeSizing.imeUiHeight()),
@@ -524,19 +609,29 @@ fun ClipboardInputLayout(
@Composable
private fun ClipCategoryTitle(
text: String,
style: SnyggPropertySet,
modifier: Modifier = Modifier,
) {
SnyggText(FlorisImeUi.ClipboardSubheader.elementName,
modifier = modifier.fillMaxWidth(),
val context = LocalContext.current
Text(
modifier = modifier
.padding(ItemMargin)
.padding(top = 8.dp)
.fillMaxWidth(),
text = text.uppercase(),
color = style.foreground.solidColor(context),
fontWeight = FontWeight.Bold,
fontSize = style.fontSize.spSize() safeTimes 0.8f,
)
}
@Composable
private fun ClipTextItemDescription(
text: String,
style: SnyggPropertySet,
modifier: Modifier = Modifier,
): Unit = with(LocalDensity.current) {
val context = LocalContext.current
val iconId: Int?
val description: String?
when {
@@ -558,16 +653,27 @@ private fun ClipTextItemDescription(
}
}
if (iconId != null && description != null) {
SnyggRow(
modifier = modifier,
Row(
modifier = modifier
.padding(DescriptionPadding)
.offset(y = DescriptionPadding.calculateTopPadding()),
verticalAlignment = Alignment.CenterVertically,
) {
SnyggIcon(
val fontSize = style.fontSize.spSize()
Icon(
modifier = Modifier
.padding(end = 8.dp)
.requiredSize(fontSize.toDp()),
painter = painterResource(id = iconId),
contentDescription = null,
tint = style.foreground.solidColor(context),
)
SnyggText(
Text(
modifier = Modifier.weight(1f),
text = description,
color = style.foreground.solidColor(context),
fontSize = fontSize safeTimes 0.8f,
fontStyle = FontStyle.Italic,
)
}
}
@@ -577,19 +683,29 @@ private fun ClipTextItemDescription(
private fun PopupAction(
@DrawableRes iconId: Int,
text: String,
style: SnyggPropertySet,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
SnyggRow(FlorisImeUi.ClipboardItemPopupAction.elementName,
modifier = modifier.rippleClickable(onClick = onClick),
val context = LocalContext.current
Row(
modifier = modifier
.width(ItemWidth)
.rippleClickable(onClick = onClick)
.padding(all = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SnyggIcon(FlorisImeUi.ClipboardItemPopupActionIcon.elementName,
Icon(
modifier = Modifier.padding(end = 8.dp),
painter = painterResource(iconId),
contentDescription = null,
tint = style.foreground.solidColor(context),
)
SnyggText(FlorisImeUi.ClipboardItemPopupActionText.elementName,
Text(
modifier = Modifier.weight(1f),
text = text,
color = style.foreground.solidColor(context),
fontSize = style.fontSize.spSize(),
)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -254,20 +254,14 @@ 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)
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())
val itemsToRemove = nonPinnedItems.filter { it.creationTimestampMs < expiryTime }
if (itemsToRemove.isNotEmpty()) {
ioScope.launch {
clipHistoryDao?.delete(itemsToRemove)
}
}
}
}

View File

@@ -1,19 +1,3 @@
/*
* 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
@@ -120,8 +104,9 @@ class FlorisCopyToClipboardActivity : ComponentActivity() {
setContent {
ProvideLocalizedResources(this, forceLayoutDirection = LayoutDirection.Ltr) {
val theme by prefs.other.settingsTheme.observeAsState()
FlorisAppTheme(theme) {
val theme by prefs.advanced.settingsTheme.observeAsState()
val isMaterialYouAware by prefs.advanced.useMaterialYou.observeAsState()
FlorisAppTheme(theme, isMaterialYouAware) {
BottomSheet {
Row {
Text(
@@ -161,9 +146,7 @@ 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-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -242,7 +242,6 @@ data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor
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
}
@@ -254,7 +253,6 @@ data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor
result = 31 * result + (uri?.hashCode() ?: 0)
result = 31 * result + creationTimestampMs.hashCode()
result = 31 * result + mimeTypes.contentHashCode()
result = 31 * result + isSensitive.hashCode()
return result
}

View File

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

View File

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

View File

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

View File

@@ -1,109 +0,0 @@
/*
* 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.core
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeDrawingPadding
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.RadioButtonChecked
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.subtypeManager
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
@Composable
fun SelectSubtypePanel(modifier: Modifier = Modifier) {
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
val subtypeManager by context.subtypeManager()
val listState = rememberLazyListState()
val subtypes by subtypeManager.subtypesFlow.collectAsState()
val currentlySelected = subtypeManager.activeSubtype.id
SnyggColumn(FlorisImeUi.SubtypePanel.elementName, modifier = modifier.safeDrawingPadding()) {
SnyggRow(
elementName = FlorisImeUi.SubtypePanelHeader.elementName,
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
SnyggText(
modifier = Modifier
.fillMaxWidth()
.clickable(false) {},
text = stringRes(R.string.select_subtype_panel__header),
)
}
Box {
LazyColumn(
state = listState,
) {
items(
subtypes,
key = {
it.id
}
) {
JetPrefListItem(
modifier = Modifier
.fillMaxWidth()
.rippleClickable {
subtypeManager.switchToSubtypeById(it.id)
keyboardManager.activeState.isSubtypeSelectionVisible = false
},
icon = {
if (currentlySelected == it.id) {
Icon(Icons.Default.RadioButtonChecked, null)
} else {
Icon(Icons.Default.RadioButtonUnchecked, null)
}
},
text = it.primaryLocale.displayName(),
colors = ListItemDefaults.colors(),
)
}
}
}
}
}
fun KeyboardState.isSubtypeSelectionShowing(): Boolean {
return isSubtypeSelectionVisible
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -226,11 +227,4 @@ class SubtypeManager(context: Context) {
prefs.localization.activeSubtypeId.set(newActiveSubtype.id)
activeSubtype = newActiveSubtype
}
fun switchToSubtypeById(id: Long) {
if (subtypes.any { it.id == id }) {
activeSubtype = getSubtypeById(id)!!
prefs.localization.activeSubtypeId.set(id)
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2025 The FlorisBoard Contributors
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -281,7 +281,7 @@ abstract class AbstractEditorInstance(context: Context) {
abstract fun determineComposer(composerName: ExtensionComponentName): Composer
protected open fun shouldDetermineComposingRegion(editorInfo: FlorisEditorInfo): Boolean {
return editorInfo.isRichInputEditor && !editorInfo.inputAttributes.flagTextNoSuggestions
return editorInfo.isRichInputEditor
}
private suspend fun determineLocalComposing(
@@ -634,7 +634,7 @@ abstract class AbstractEditorInstance(context: Context) {
suspend fun popUntilOrNull(predicate: (EditorContent) -> Boolean): EditorContent? {
return list.withLock { list ->
while (list.isNotEmpty()) {
val item = list.removeAt(0)
val item = list.removeFirst()
if (predicate(item)) return@withLock item
}
return@withLock null

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