Compare commits
90 Commits
v0.3.14-be
...
v0.3.14-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5aac7ac14 | ||
|
|
9f58088545 | ||
|
|
b684f1759d | ||
|
|
aa7a264d6c | ||
|
|
6ac537c517 | ||
|
|
2386ae7749 | ||
|
|
7d559acfae | ||
|
|
7783b9b218 | ||
|
|
548f7d7b1e | ||
|
|
4629c07812 | ||
|
|
3b2b7da841 | ||
|
|
25ef53510a | ||
|
|
0064f248d3 | ||
|
|
0c721696f2 | ||
|
|
131ab6214d | ||
|
|
70bc7a1236 | ||
|
|
6c88716a2a | ||
|
|
ff3c37e360 | ||
|
|
58bab443c4 | ||
|
|
a8b0a6d555 | ||
|
|
0a430b4b0a | ||
|
|
8b76c5ce3b | ||
|
|
c81f5f7015 | ||
|
|
cf6b186269 | ||
|
|
bd25ddb92e | ||
|
|
62bdd31af3 | ||
|
|
d4af89bf99 | ||
|
|
1c38a42c0b | ||
|
|
6d1ebb74fb | ||
|
|
9673e6de5c | ||
|
|
ab709e2c69 | ||
|
|
9144708cf0 | ||
|
|
38136de39d | ||
|
|
beb800a76e | ||
|
|
aab738526a | ||
|
|
86bdad61a4 | ||
|
|
e4c56cab03 | ||
|
|
cef1c4e3f6 | ||
|
|
b2721c9faa | ||
|
|
ab4ae62ffe | ||
|
|
74244bab74 | ||
|
|
57112ae692 | ||
|
|
fd1314ccba | ||
|
|
45d99df104 | ||
|
|
17b87f6543 | ||
|
|
d62e82569d | ||
|
|
dc5e00cc07 | ||
|
|
6402511d38 | ||
|
|
83c1f70077 | ||
|
|
f3375f48ef | ||
|
|
e1b911086b | ||
|
|
b60c0cef51 | ||
|
|
0c42185700 | ||
|
|
ee3c779b17 | ||
|
|
b5e6655c84 | ||
|
|
e1b45b9193 | ||
|
|
588713bd55 | ||
|
|
43ad452174 | ||
|
|
2cf9146536 | ||
|
|
f81331baed | ||
|
|
9c9c3b9428 | ||
|
|
4b64d81c21 | ||
|
|
e826f600f0 | ||
|
|
0f845a9784 | ||
|
|
a2805bedca | ||
|
|
d860bbfb90 | ||
|
|
70e2d34410 | ||
|
|
1c49a11824 | ||
|
|
cd2a0000c0 | ||
|
|
1304e49eb4 | ||
|
|
9658cecb88 | ||
|
|
4c23d5bafc | ||
|
|
82238c8c1a | ||
|
|
ead74e1c26 | ||
|
|
d58371be81 | ||
|
|
844d194533 | ||
|
|
84abc929d0 | ||
|
|
7497470875 | ||
|
|
cc5df41daa | ||
|
|
d87b290a32 | ||
|
|
a0f859ad03 | ||
|
|
c86892ec0b | ||
|
|
c85fea0799 | ||
|
|
765e34a01d | ||
|
|
96e7f2eeac | ||
|
|
23dddfd16e | ||
|
|
e2318d0af1 | ||
|
|
ef3b840dce | ||
|
|
9b9c5fa70e | ||
|
|
6f0216cf9f |
@@ -18,10 +18,10 @@ fully respecting your privacy. Currently in early-beta state.
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<p><i>Major versions only, 1 release per 1-3 months</i><br><br>Updates are more polished, new features are matured and tested through to ensure a stable experience.</p>
|
||||
<p><i>Major versions only, 1 release per 1-5 months</i><br><br>Updates are more polished, new features are matured and tested through to ensure a stable experience.</p>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<p><i>Beta versions, 1-2 releases per week</i><br><br>Updates contain new features that may not be fully matured yet and bugs are more likely to occur. Allows you to give early feedback.</p>
|
||||
<p><i>Beta versions, up to 1-2 releases per week</i><br><br>Updates contain new features that may not be fully matured yet and bugs are more likely to occur. Allows you to give early feedback.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -30,8 +30,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdk = 23
|
||||
targetSdk = 30
|
||||
versionCode = 64
|
||||
targetSdk = 31
|
||||
versionCode = 66
|
||||
versionName = "0.3.14"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -75,7 +75,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.1.0-rc01"
|
||||
kotlinCompilerExtensionVersion = "1.1.0-rc03"
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -106,7 +106,7 @@ android {
|
||||
create("beta") // Needed because by default the "beta" BuildType does not exist
|
||||
named("beta").configure {
|
||||
applicationIdSuffix = ".beta"
|
||||
versionNameSuffix = "-beta08"
|
||||
versionNameSuffix = "-beta10"
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
|
||||
@@ -133,10 +133,6 @@ android {
|
||||
it.useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
isAbortOnError = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
@@ -148,26 +144,28 @@ dependencies {
|
||||
implementation("androidx.activity:activity-ktx:1.4.0")
|
||||
implementation("androidx.autofill:autofill:1.1.0")
|
||||
implementation("androidx.collection:collection-ktx:1.2.0")
|
||||
implementation("androidx.compose.material:material:1.1.0-rc01")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:1.1.0-rc01")
|
||||
implementation("androidx.compose.ui:ui:1.1.0-rc01")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.1.0-rc01")
|
||||
implementation("androidx.compose.material:material:1.1.0-rc03")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:1.1.0-rc03")
|
||||
implementation("androidx.compose.ui:ui:1.1.0-rc03")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.1.0-rc03")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
||||
implementation("androidx.navigation:navigation-compose:2.4.0")
|
||||
implementation("com.google.accompanist:accompanist-flowlayout:0.20.2")
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.20.2")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta02")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta02")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta02")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
|
||||
implementation("androidx.room:room-runtime:2.4.0")
|
||||
kapt("androidx.room:room-compiler:2.4.0")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta06")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta06")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta06")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
|
||||
implementation("androidx.room:room-runtime:2.4.1")
|
||||
kapt("androidx.room:room-compiler:2.4.1")
|
||||
|
||||
testImplementation("io.kotest:kotest-runner-junit5:4.6.3")
|
||||
testImplementation("io.kotest:kotest-assertions-core:4.6.3")
|
||||
testImplementation("io.kotest:kotest-property:4.6.3")
|
||||
testImplementation("io.kotest.extensions:kotest-extensions-robolectric:0.4.0")
|
||||
testImplementation("io.kotest:kotest-runner-junit5:5.1.0")
|
||||
testImplementation("io.kotest:kotest-assertions-core:5.1.0")
|
||||
testImplementation("io.kotest:kotest-property:5.1.0")
|
||||
testImplementation("io.kotest.extensions:kotest-extensions-robolectric:0.5.0")
|
||||
testImplementation("nl.jqno.equalsverifier:equalsverifier:3.8.3")
|
||||
|
||||
androidTestImplementation("androidx.test.ext", "junit", "1.1.2")
|
||||
androidTestImplementation("androidx.test.espresso", "espresso-core", "3.3.0")
|
||||
|
||||
@@ -36,12 +36,15 @@
|
||||
|
||||
<application
|
||||
android:name="dev.patrickgold.florisboard.FlorisApplication"
|
||||
android:allowBackup="false"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/floris_app_name"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/FlorisAppTheme">
|
||||
android:theme="@style/FlorisAppTheme"
|
||||
tools:targetApi="s">
|
||||
|
||||
<!-- IME service -->
|
||||
<service
|
||||
@@ -132,6 +135,17 @@
|
||||
android:exported="false">
|
||||
</provider>
|
||||
|
||||
<!-- Default file provider to share files from the "files" or "cache" dir -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider.file"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -235,6 +235,18 @@
|
||||
"authors": [ "patrickgold" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "rusyn",
|
||||
"label": "Rusyn",
|
||||
"authors": [ "svvvst" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "rusyn_us",
|
||||
"label": "Rusyn (Phonetic)",
|
||||
"authors": [ "svvvst" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "sangaline",
|
||||
"label": "Sangaline",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
[
|
||||
[
|
||||
{ "code": 1602, "label": "ق", "popup": {
|
||||
"main": { "code": 1647, "label": "ٯ" }
|
||||
} },
|
||||
{ "code": 1608, "label": "و", "popup": {
|
||||
"main": { "code": -255, "label": "وو" }
|
||||
} },
|
||||
{ "code": 1749, "label": "ﻪ", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
{ "code": 1749, "label": "ە", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
} },
|
||||
{ "code": 1585, "label": "ر", "popup": {
|
||||
"main": { "code": 1685, "label": "ڕ" }
|
||||
@@ -15,50 +15,51 @@
|
||||
{ "code": 1578, "label": "ت", "popup": {
|
||||
"main": { "code": 1591, "label": "ط" }
|
||||
} },
|
||||
{ "code": 1740, "label": "ی", "popup": {
|
||||
"main": { "code": 1742, "label": "ێ" }
|
||||
} },
|
||||
{ "code": 1740, "label": "ی" },
|
||||
{ "code": 1742, "label": "ێ" },
|
||||
{ "code": 1574, "label": "ﺋ", "popup": {
|
||||
"main": { "code": 1569, "label": "ء" }
|
||||
} },
|
||||
{ "code": 1593, "label": "ع", "popup": {
|
||||
"main": { "code": 1594, "label": "غ" }
|
||||
} },
|
||||
{ "code": 1734, "label": "ۆ" },
|
||||
{ "code": 1662, "label": "پ", "popup": {
|
||||
"main": { "code": 1579, "label": "ث" }
|
||||
} }
|
||||
],
|
||||
[
|
||||
{"code": 1575, "label": "ا"},
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1583, "label": "د", "popup": {
|
||||
"main": {"code": 1584, "label": "ذ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1575, "label": "ا"},
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1583, "label": "د", "popup": {
|
||||
"main": {"code": 1584, "label": "ذ" }
|
||||
} },
|
||||
{ "code": 1601, "label": "ف" , "popup": {
|
||||
"main": {"code": 1700, "label": "ڤ" }
|
||||
} },
|
||||
{ "code": 1601, "label": "ف" , "popup": {
|
||||
"main": {"code": 1700, "label": "ڤ" }
|
||||
} },
|
||||
{ "code": 1607, "label": "ھ" },
|
||||
{ "code": 1688, "label": "ژ", "popup": {
|
||||
"main": { "code": 1600, "label": "▬" }
|
||||
} },
|
||||
{ "code": 1604, "label": "ل", "popup": {
|
||||
"main": { "code": 1717, "label": "ڵ" }
|
||||
} },
|
||||
{ "code": 1705, "label": "ک" },
|
||||
{ "code": 1711, "label": "گ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1586, "label": "ز", "popup": {
|
||||
"main": {"code": 1592, "label": "ظ" }
|
||||
} },
|
||||
{ "code": 1582, "label": "خ" },
|
||||
{ "code": 1580, "label": "ج" },
|
||||
{ "code": 1670, "label": "چ" },
|
||||
{ "code": 1581, "label": "ح" },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" }
|
||||
{ "code": 1726, "label": "ھ" },
|
||||
{ "code": 1688, "label": "ژ", "popup": {
|
||||
"main": { "code": 1600, "label": "━" }
|
||||
} },
|
||||
{ "code": 1604, "label": "ل", "popup": {
|
||||
"main": { "code": 1717, "label": "ڵ" }
|
||||
} },
|
||||
{ "code": 1705, "label": "ک" },
|
||||
{ "code": 1711, "label": "گ" , "popup": {
|
||||
"main": { "code": 1594, "label": "غ" }
|
||||
} }
|
||||
],
|
||||
[
|
||||
{ "code": 1586, "label": "ز", "popup": {
|
||||
"main": {"code": 1592, "label": "ظ" }
|
||||
} },
|
||||
{ "code": 1582, "label": "خ" },
|
||||
{ "code": 1580, "label": "ج" },
|
||||
{ "code": 1670, "label": "چ" },
|
||||
{ "code": 1581, "label": "ح" },
|
||||
{ "code": 1593, "label": "ع", "popup": {
|
||||
"main": { "code": 1551, "label": "؏" }
|
||||
} },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" }
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
[
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1081, "label": "й" },
|
||||
{ "$": "auto_text_key", "code": 1094, "label": "ц" },
|
||||
{ "$": "auto_text_key", "code": 1091, "label": "у" },
|
||||
{ "$": "auto_text_key", "code": 1082, "label": "к" },
|
||||
{ "$": "auto_text_key", "code": 1077, "label": "е" },
|
||||
{ "$": "auto_text_key", "code": 1085, "label": "н" },
|
||||
{ "$": "auto_text_key", "code": 1075, "label": "г" },
|
||||
{ "$": "auto_text_key", "code": 1096, "label": "ш" },
|
||||
{ "$": "auto_text_key", "code": 1097, "label": "щ" },
|
||||
{ "$": "auto_text_key", "code": 1079, "label": "з" },
|
||||
{ "$": "auto_text_key", "code": 1093, "label": "х" },
|
||||
{ "$": "auto_text_key", "code": 1031, "label": "ї" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1092 , "label": "ф" },
|
||||
|
||||
{ "$": "auto_text_key", "code": 1110 , "label": "і" },
|
||||
{ "$": "auto_text_key", "code": 1074 , "label": "в" },
|
||||
{ "$": "auto_text_key", "code": 1072 , "label": "а" },
|
||||
{ "$": "auto_text_key", "code": 1087 , "label": "п" },
|
||||
|
||||
|
||||
{ "$": "auto_text_key", "code": 1088 , "label": "р" },
|
||||
{ "$": "auto_text_key", "code": 1086 , "label": "о" },
|
||||
|
||||
{ "$": "auto_text_key", "code": 1083 , "label": "л" },
|
||||
{ "$": "auto_text_key", "code": 1076 , "label": "д" },
|
||||
{ "$": "auto_text_key", "code": 1078 , "label": "ж" },
|
||||
{ "$": "auto_text_key", "code": 1108 , "label": "є" },
|
||||
{ "$": "auto_text_key", "code": 1067 , "label": "ы" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1169 , "label": "ґ" },
|
||||
{ "$": "auto_text_key", "code": 1103 , "label": "я" },
|
||||
{ "$": "auto_text_key", "code": 1095 , "label": "ч" },
|
||||
{ "$": "auto_text_key", "code": 1089 , "label": "с" },
|
||||
{ "$": "auto_text_key", "code": 1084 , "label": "м" },
|
||||
{ "$": "auto_text_key", "code": 1080 , "label": "и" },
|
||||
{ "$": "auto_text_key", "code": 1090 , "label": "т" },
|
||||
{ "$": "auto_text_key", "code": 1100 , "label": "ь" },
|
||||
{ "$": "auto_text_key", "code": 1073 , "label": "б" },
|
||||
{ "$": "auto_text_key", "code": 1102 , "label": "ю" },
|
||||
{ "$": "auto_text_key", "code": 1025 , "label": "ё" }
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
[
|
||||
[
|
||||
|
||||
{ "$": "auto_text_key", "code": 1094, "label": "ц" },
|
||||
{ "$": "auto_text_key", "code": 1108 , "label": "є" },
|
||||
|
||||
{ "$": "auto_text_key", "code": 1077, "label": "е" },
|
||||
{ "$": "auto_text_key", "code": 1088 , "label": "р" },
|
||||
{ "$": "auto_text_key", "code": 1090 , "label": "т" },
|
||||
{ "$": "auto_text_key", "code": 1081, "label": "й" },
|
||||
{ "$": "auto_text_key", "code": 1091, "label": "у" },
|
||||
{ "$": "auto_text_key", "code": 1102 , "label": "ю" },
|
||||
{ "$": "auto_text_key", "code": 1110 , "label": "і" },
|
||||
{ "$": "auto_text_key", "code": 1031, "label": "ї" },
|
||||
{ "$": "auto_text_key", "code": 1086 , "label": "о" },
|
||||
{ "$": "auto_text_key", "code": 1025 , "label": "ё" },
|
||||
{ "$": "auto_text_key", "code": 1087 , "label": "п" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1103 , "label": "я" },
|
||||
{ "$": "auto_text_key", "code": 1072 , "label": "а" },
|
||||
{ "$": "auto_text_key", "code": 1089 , "label": "с" },
|
||||
{ "$": "auto_text_key", "code": 1076 , "label": "д" },
|
||||
{ "$": "auto_text_key", "code": 1092 , "label": "ф" },
|
||||
{ "$": "auto_text_key", "code": 1169 , "label": "ґ" },
|
||||
{ "$": "auto_text_key", "code": 1067 , "label": "ы" },
|
||||
{ "$": "auto_text_key", "code": 1075, "label": "г" },
|
||||
{ "$": "auto_text_key", "code": 1078 , "label": "ж" },
|
||||
{ "$": "auto_text_key", "code": 1082, "label": "к" },
|
||||
{ "$": "auto_text_key", "code": 1083 , "label": "л" },
|
||||
{ "$": "auto_text_key", "code": 1096, "label": "ш" },
|
||||
{ "$": "auto_text_key", "code": 1097, "label": "щ" }
|
||||
],
|
||||
[
|
||||
|
||||
{ "$": "auto_text_key", "code": 1079, "label": "з" },
|
||||
{ "$": "auto_text_key", "code": 1093, "label": "х" },
|
||||
{ "$": "auto_text_key", "code": 1095 , "label": "ч" },
|
||||
{ "$": "auto_text_key", "code": 1074 , "label": "в" },
|
||||
{ "$": "auto_text_key", "code": 1073 , "label": "б" },
|
||||
{ "$": "auto_text_key", "code": 1085, "label": "н" },
|
||||
{ "$": "auto_text_key", "code": 1084 , "label": "м" },
|
||||
{ "$": "auto_text_key", "code": 1080 , "label": "и" },
|
||||
{ "$": "auto_text_key", "code": 1100 , "label": "ь" }
|
||||
]
|
||||
]
|
||||
@@ -150,6 +150,10 @@
|
||||
"id": "ru",
|
||||
"authors": [ "williamtheaker", "33kk" ]
|
||||
},
|
||||
{
|
||||
"id": "rue",
|
||||
"authors": [ "svvvst" ]
|
||||
},
|
||||
{
|
||||
"id": "sk",
|
||||
"authors": [ "stefan-misik", "majso" ]
|
||||
@@ -467,6 +471,15 @@
|
||||
"characters": "org.florisboard.layouts:jcuken_russian"
|
||||
}
|
||||
},
|
||||
{
|
||||
"languageTag": "rue",
|
||||
"composer": "org.florisboard.composers:appender",
|
||||
"currencySet": "org.florisboard.currencysets:euro",
|
||||
"popupMapping": "org.florisboard.localization:rue",
|
||||
"preferred": {
|
||||
"characters": "org.florisboard.layouts:rusyn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"languageTag": "uk",
|
||||
"composer": "org.florisboard.composers:appender",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"all": {
|
||||
"е": {
|
||||
"main": { "$": "auto_text_key", "code": 234, "label": "ê" },
|
||||
"relevant": [{ "$": "auto_text_key", "code": 1105, "label": "ё" }]
|
||||
},
|
||||
"у": {
|
||||
"main": { "$": "auto_text_key", "code": 1263, "label": "ӯ" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 1118, "label": "ў" },
|
||||
{ "$": "auto_text_key", "code": 1265, "label": "ӱ" },
|
||||
{ "$": "auto_text_key", "code": 375, "label": "ŷ" }
|
||||
]
|
||||
},
|
||||
"г": {
|
||||
"main": { "$": "auto_text_key", "code": 1169, "label": "ґ" }
|
||||
},
|
||||
"і": {
|
||||
"main": { "$": "auto_text_key", "code": 1123, "label": "î" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 1123, "label": "ѣ" },
|
||||
{ "$": "auto_text_key", "code": 1111, "label": "ї" }
|
||||
]
|
||||
},
|
||||
"о": {
|
||||
"main": { "$": "auto_text_key", "code": 333, "label": "ō" },
|
||||
"relevant": [{ "$": "auto_text_key", "code": 244, "label": "ô" }]
|
||||
},
|
||||
"ь": {
|
||||
"main": { "$": "auto_text_key", "code": 1098, "label": "ъ" }
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@
|
||||
"--surface": "#ffffff",
|
||||
"--surface-variant": "#f5f5f5",
|
||||
|
||||
"--on-primary": "#000000",
|
||||
"--on-secondary": "#000000",
|
||||
"--on-background": "#000000",
|
||||
"--on-surface": "#000000"
|
||||
"--on-background": "#121212",
|
||||
"--on-surface": "#000000",
|
||||
"--on-surface-variant": "#5f5f5f",
|
||||
|
||||
"--shape": "rounded-corner(8dp, 8dp, 8dp, 8dp)",
|
||||
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
},
|
||||
|
||||
"keyboard": {
|
||||
@@ -22,7 +24,8 @@
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shape": "rounded-corner(20%, 20%, 20%, 20%)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key:pressed": {
|
||||
"background": "var(--surface-variant)",
|
||||
@@ -30,30 +33,31 @@
|
||||
},
|
||||
"key[code={c:enter}]": {
|
||||
"background": "var(--primary)",
|
||||
"foreground": "#ffffff"
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:enter}]:pressed": {
|
||||
"background": "var(--primary-variant)",
|
||||
"foreground": "#ffffff"
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:shift}][mode={m:capslock}]": {
|
||||
"foreground": "var(--secondary)"
|
||||
},
|
||||
"key[code={c:space}]": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#909090",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-hint": {
|
||||
"background": "transparent",
|
||||
"foreground": "#b8b8b8",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-popup": {
|
||||
"background": "#eeeeee",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shape": "rounded-corner(20%, 20%, 20%, 20%)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key-popup:focus": {
|
||||
"background": "#bdbdbd",
|
||||
@@ -69,13 +73,19 @@
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
"clipboard-item-popup": {
|
||||
"background": "var(--surface)",
|
||||
"background": "var(--surface-variant)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
|
||||
"glide-trail": {
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
@@ -89,12 +99,13 @@
|
||||
"smartbar-primary-action-row-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "rounded-corner(50%, 50%, 50%, 50%)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-primary-secondary-row-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "#121212",
|
||||
"shape": "rounded-corner(50%, 50%, 50%, 50%)"
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
|
||||
"smartbar-secondary-row": {
|
||||
@@ -106,8 +117,8 @@
|
||||
},
|
||||
"smartbar-action-button": {
|
||||
"background": "transparent",
|
||||
"foreground": "#121212",
|
||||
"shape": "rounded-corner(50%, 50%, 50%, 50%)"
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
|
||||
"smartbar-candidate-row": {
|
||||
@@ -115,37 +126,37 @@
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "#121212",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rectangle()"
|
||||
},
|
||||
"smartbar-candidate-word:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#121212"
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-clip": {
|
||||
"background": "transparent",
|
||||
"foreground": "#121212",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
|
||||
},
|
||||
"smartbar-candidate-clip:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#121212"
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-spacer": {
|
||||
"foreground": "#ffffff40"
|
||||
"foreground": "var(--surface)"
|
||||
},
|
||||
|
||||
"smartbar-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "#121212",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "rounded-corner(20%, 20%, 20%, 20%)"
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#121212"
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"background": "transparent",
|
||||
@@ -153,6 +164,6 @@
|
||||
},
|
||||
|
||||
"system-nav-bar": {
|
||||
"background": "#e0e0e0"
|
||||
"background": "var(--background)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
"--surface": "#424242",
|
||||
"--surface-variant": "#616161",
|
||||
|
||||
"--on-primary": "#ffffff",
|
||||
"--on-secondary": "#ffffff",
|
||||
"--on-background": "#ffffff",
|
||||
"--on-surface": "#ffffff"
|
||||
"--on-background": "#dcdcdc",
|
||||
"--on-surface": "#ffffff",
|
||||
"--on-surface-variant": "#a0a0a0",
|
||||
|
||||
"--shape": "rounded-corner(8dp, 8dp, 8dp, 8dp)",
|
||||
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
},
|
||||
|
||||
"keyboard": {
|
||||
@@ -22,7 +24,8 @@
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shape": "rounded-corner(20%, 20%, 20%, 20%)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key:pressed": {
|
||||
"background": "var(--surface-variant)",
|
||||
@@ -41,19 +44,20 @@
|
||||
},
|
||||
"key[code={c:space}]": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#909090",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-hint": {
|
||||
"background": "transparent",
|
||||
"foreground": "#b8b8b8",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-popup": {
|
||||
"background": "#757575",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shape": "rounded-corner(20%, 20%, 20%, 20%)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key-popup:focus": {
|
||||
"background": "#bdbdbd",
|
||||
@@ -69,13 +73,19 @@
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
"clipboard-item-popup": {
|
||||
"background": "#757575",
|
||||
"background": "var(--surface-variant)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
|
||||
"glide-trail": {
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
@@ -89,12 +99,13 @@
|
||||
"smartbar-primary-action-row-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "rounded-corner(50%, 50%, 50%, 50%)"
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-primary-secondary-row-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "#909090",
|
||||
"shape": "rounded-corner(50%, 50%, 50%, 50%)"
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
|
||||
"smartbar-secondary-row": {
|
||||
@@ -106,8 +117,8 @@
|
||||
},
|
||||
"smartbar-action-button": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc",
|
||||
"shape": "rounded-corner(50%, 50%, 50%, 50%)"
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
|
||||
"smartbar-candidate-row": {
|
||||
@@ -115,44 +126,44 @@
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rectangle()"
|
||||
},
|
||||
"smartbar-candidate-word:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#dcdcdc"
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-clip": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
|
||||
},
|
||||
"smartbar-candidate-clip:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#dcdcdc"
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-spacer": {
|
||||
"foreground": "#ffffff40"
|
||||
"foreground": "var(--surface)"
|
||||
},
|
||||
|
||||
"smartbar-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "rounded-corner(20%, 20%, 20%, 20%)"
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "#dcdcdc"
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--surface)"
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
|
||||
"system-nav-bar": {
|
||||
"background": "#212121"
|
||||
"background": "var(--background)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import dev.patrickgold.florisboard.ime.nlp.NlpManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
|
||||
import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.io.deleteContentsRecursively
|
||||
import dev.patrickgold.jetpref.datastore.JetPrefManager
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import java.io.File
|
||||
import kotlin.Exception
|
||||
|
||||
@@ -80,7 +80,7 @@ class FlorisApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
try {
|
||||
JetPrefManager.init(saveIntervalMs = 1_000)
|
||||
JetPref.configure(saveIntervalMs = 500)
|
||||
Flog.install(
|
||||
context = this,
|
||||
isFloggingEnabled = BuildConfig.DEBUG,
|
||||
@@ -93,12 +93,12 @@ class FlorisApplication : Application() {
|
||||
if (AndroidVersion.ATLEAST_API24_N && !UserManagerCompat.isUserUnlocked(this)) {
|
||||
val context = createDeviceProtectedStorageContext()
|
||||
initICU(context)
|
||||
prefs.initializeForContext(context)
|
||||
prefs.initializeBlocking(context)
|
||||
registerReceiver(BootComplete(), IntentFilter(Intent.ACTION_USER_UNLOCKED))
|
||||
} else {
|
||||
initICU(this)
|
||||
cacheDir?.deleteContentsRecursively()
|
||||
prefs.initializeForContext(this)
|
||||
prefs.initializeBlocking(this)
|
||||
clipboardManager.value.initializeForContext(this)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class FlorisApplication : Application() {
|
||||
flogError { e.toString() }
|
||||
}
|
||||
cacheDir?.deleteContentsRecursively()
|
||||
prefs.initializeForContext(this@FlorisApplication)
|
||||
prefs.initializeBlocking(this@FlorisApplication)
|
||||
clipboardManager.value.initializeForContext(this@FlorisApplication)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ import dev.patrickgold.florisboard.ime.text.TextInputLayout
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.snygg.ui.SnyggSurface
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -185,6 +184,7 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val keyboardManager by keyboardManager()
|
||||
private val themeManager by themeManager()
|
||||
private val nlpManager by nlpManager()
|
||||
|
||||
private val activeEditorInstance by lazy { EditorInstance(this) }
|
||||
@@ -331,7 +331,7 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
flogInfo(LogTopic.IMS_EVENTS) {
|
||||
"Creating inline suggestions request because Smartbar and inline suggestions are enabled."
|
||||
}
|
||||
val stylesBundle = ThemeManager.createInlineSuggestionUiStyleBundle(this)
|
||||
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(this)
|
||||
val spec = InlinePresentationSpec.Builder(InlineSuggestionUiSmallestSize, InlineSuggestionUiBiggestSize)
|
||||
.setStyle(stylesBundle)
|
||||
.build()
|
||||
@@ -430,7 +430,11 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun BoxScope.ImeUi() {
|
||||
val keyboardStyle = FlorisImeTheme.style.get(FlorisImeUi.Keyboard)
|
||||
val activeState by keyboardManager.observeActiveState()
|
||||
val keyboardStyle = FlorisImeTheme.style.get(
|
||||
element = FlorisImeUi.Keyboard,
|
||||
mode = activeState.inputMode.value,
|
||||
)
|
||||
SnyggSurface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -472,7 +476,6 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
.weight(keyboardWeight)
|
||||
.wrapContentHeight(),
|
||||
) {
|
||||
val activeState by keyboardManager.observeActiveState()
|
||||
when (activeState.imeUiMode) {
|
||||
ImeUiMode.TEXT -> TextInputLayout()
|
||||
ImeUiMode.MEDIA -> {}
|
||||
|
||||
@@ -144,12 +144,6 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
prefs.forceSyncToDisk()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun AppContent() {
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.prefs
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import dev.patrickgold.florisboard.app.AppTheme
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.DisplayColorsAs
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
@@ -32,12 +31,12 @@ import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
||||
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.util.VersionName
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
|
||||
import dev.patrickgold.jetpref.datastore.preferenceModel
|
||||
import java.time.LocalTime
|
||||
|
||||
fun florisPreferenceModel() = preferenceModel(AppPrefs::class, ::AppPrefs)
|
||||
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
|
||||
|
||||
class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
val advanced = Advanced()
|
||||
@@ -355,6 +354,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "keyboard__utility_key_action",
|
||||
default = UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
|
||||
)
|
||||
val spaceBarLanguageDisplayEnabled = boolean(
|
||||
key = "keyboard__space_bar_language_display_enabled",
|
||||
default = true,
|
||||
)
|
||||
val fontSizeMultiplierPortrait = int(
|
||||
key = "keyboard__font_size_multiplier_portrait",
|
||||
default = 100,
|
||||
@@ -555,15 +558,21 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
default = extCoreTheme("floris_night"),
|
||||
serializer = ExtensionComponentName.Serializer,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val sunriseTime = localTime(
|
||||
key = "theme__sunrise_time",
|
||||
default = LocalTime.of(6, 0),
|
||||
//val sunriseTime = localTime(
|
||||
// key = "theme__sunrise_time",
|
||||
// default = LocalTime.of(6, 0),
|
||||
//)
|
||||
//val sunsetTime = localTime(
|
||||
// key = "theme__sunset_time",
|
||||
// default = LocalTime.of(18, 0),
|
||||
//)
|
||||
val editorDisplayColorsAs = enum(
|
||||
key = "theme__editor_display_colors_as",
|
||||
default = DisplayColorsAs.HEX8,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val sunsetTime = localTime(
|
||||
key = "theme__sunset_time",
|
||||
default = LocalTime.of(18, 0),
|
||||
val editorLevel = enum(
|
||||
key = "theme__editor_level",
|
||||
default = SnyggLevel.ADVANCED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.navigation.compose.composable
|
||||
import dev.patrickgold.florisboard.app.ui.devtools.AndroidLocalesScreen
|
||||
import dev.patrickgold.florisboard.app.ui.devtools.AndroidSettingsScreen
|
||||
import dev.patrickgold.florisboard.app.ui.devtools.DevtoolsScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionEditScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionExportScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionImportScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionImportScreenType
|
||||
@@ -33,6 +34,8 @@ import dev.patrickgold.florisboard.app.ui.settings.about.AboutScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.about.ProjectLicenseScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.about.ThirdPartyLicensesScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.AdvancedScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.BackupScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.RestoreScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.clipboard.ClipboardScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.dictionary.DictionaryScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.gestures.GesturesScreen
|
||||
@@ -97,6 +100,8 @@ object Routes {
|
||||
const val Clipboard = "settings/clipboard"
|
||||
|
||||
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"
|
||||
@@ -112,6 +117,11 @@ object Routes {
|
||||
}
|
||||
|
||||
object Ext {
|
||||
const val Edit = "ext/edit/{id}?create={serial_type}"
|
||||
fun Edit(id: String, serialType: String? = null): String {
|
||||
return Edit.curlyFormat("id" to id, "serial_type" to (serialType ?: ""))
|
||||
}
|
||||
|
||||
const val Export = "ext/export/{id}"
|
||||
fun Export(id: String) = Export.curlyFormat("id" to id)
|
||||
|
||||
@@ -177,6 +187,8 @@ object Routes {
|
||||
composable(Settings.Clipboard) { ClipboardScreen() }
|
||||
|
||||
composable(Settings.Advanced) { AdvancedScreen() }
|
||||
composable(Settings.Backup) { BackupScreen() }
|
||||
composable(Settings.Restore) { RestoreScreen() }
|
||||
|
||||
composable(Settings.About) { AboutScreen() }
|
||||
composable(Settings.ProjectLicense) { ProjectLicenseScreen() }
|
||||
@@ -189,11 +201,18 @@ object Routes {
|
||||
AndroidSettingsScreen(name)
|
||||
}
|
||||
|
||||
composable(Ext.Edit) { navBackStack ->
|
||||
val extensionId = navBackStack.arguments?.getString("id")
|
||||
val serialType = navBackStack.arguments?.getString("serial_type")
|
||||
ExtensionEditScreen(
|
||||
id = extensionId.toString(),
|
||||
createSerialType = serialType.takeIf { it != null && it.isNotBlank() },
|
||||
)
|
||||
}
|
||||
composable(Ext.Export) { navBackStack ->
|
||||
val extensionId = navBackStack.arguments?.getString("id")
|
||||
ExtensionExportScreen(id = extensionId.toString())
|
||||
}
|
||||
|
||||
composable(Ext.Import) { navBackStack ->
|
||||
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
|
||||
ExtensionImportScreenType.values().firstOrNull { it.id == typeId }
|
||||
@@ -201,7 +220,6 @@ object Routes {
|
||||
val uuid = navBackStack.arguments?.getString("uuid")?.takeIf { it != "null" }
|
||||
ExtensionImportScreen(type, uuid)
|
||||
}
|
||||
|
||||
composable(Ext.View) { navBackStack ->
|
||||
val extensionId = navBackStack.arguments?.getString("id")
|
||||
ExtensionViewScreen(id = extensionId.toString())
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
infix fun TextUnit.safeTimes(other: Float): TextUnit {
|
||||
return if (this.isUnspecified) 0.sp else this.times(other)
|
||||
}
|
||||
|
||||
infix fun TextUnit.safeTimes(other: Double): TextUnit {
|
||||
return if (this.isUnspecified) this else this.times(other)
|
||||
}
|
||||
|
||||
infix fun TextUnit.safeTimes(other: Int): TextUnit {
|
||||
return if (this.isUnspecified) this else this.times(other)
|
||||
}
|
||||
|
||||
val DpSizeSaver = Saver<Dp, Float>(
|
||||
save = { it.value },
|
||||
restore = { it.dp },
|
||||
)
|
||||
@@ -17,26 +17,21 @@
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun FlorisAppBar(
|
||||
title: String,
|
||||
backArrowVisible: Boolean,
|
||||
navigationIcon: FlorisScreenNavigationIcon?,
|
||||
actions: @Composable RowScope.() -> Unit = { },
|
||||
) {
|
||||
TopAppBar(
|
||||
navigationIcon = backNavBtn(backArrowVisible),
|
||||
navigationIcon = navigationIcon,
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
@@ -49,19 +44,3 @@ fun FlorisAppBar(
|
||||
elevation = 0.dp,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun backNavBtn(backArrowVisible: Boolean): @Composable (() -> Unit)? {
|
||||
if (!backArrowVisible) return null
|
||||
val navController = LocalNavController.current
|
||||
return {
|
||||
IconButton(
|
||||
onClick = { navController.popBackStack() },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_back),
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
@Composable
|
||||
fun RowScope.FlorisBulletSpacer(
|
||||
@@ -39,6 +40,6 @@ fun RowScope.FlorisBulletSpacer(
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.12f)),
|
||||
.background(MaterialTheme.colors.outline),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
@@ -138,6 +139,35 @@ fun FlorisIconButton(
|
||||
enabled: Boolean = true,
|
||||
iconModifier: Modifier = Modifier,
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
) {
|
||||
val contentAlpha = if (enabled) LocalContentAlpha.current else 0.14f
|
||||
val contentColor = iconColor.takeOrElse { LocalContentColor.current }
|
||||
CompositionLocalProvider(
|
||||
LocalContentAlpha provides contentAlpha,
|
||||
LocalContentColor provides contentColor,
|
||||
) {
|
||||
Icon(
|
||||
modifier = iconModifier,
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisIconButtonWithInnerPadding(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Painter,
|
||||
enabled: Boolean = true,
|
||||
iconModifier: Modifier = Modifier,
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = modifier,
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -49,13 +50,21 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
private val IconRequiredSize = 24.dp
|
||||
private val IconEndPadding = 8.dp
|
||||
|
||||
private val CardContentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
object CardDefaults {
|
||||
val IconRequiredSize = 24.dp
|
||||
val IconSpacing = 8.dp
|
||||
|
||||
private val OutlinedBoxShape = RoundedCornerShape(8.dp)
|
||||
val ContentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp)
|
||||
}
|
||||
|
||||
object BoxDefaults {
|
||||
val OutlinedBoxShape = RoundedCornerShape(8.dp)
|
||||
|
||||
val ContentPadding = PaddingValues(all = 0.dp)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@@ -65,7 +74,7 @@ fun FlorisSimpleCard(
|
||||
secondaryText: String? = null,
|
||||
backgroundColor: Color = MaterialTheme.colors.surface,
|
||||
contentColor: Color = contentColorFor(backgroundColor),
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
icon: (@Composable () -> Unit)? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
@@ -110,7 +119,7 @@ fun FlorisErrorCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showIcon: Boolean = true,
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
FlorisSimpleCard(
|
||||
@@ -120,8 +129,8 @@ fun FlorisErrorCard(
|
||||
onClick = onClick,
|
||||
icon = if (showIcon) ({ Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = IconEndPadding)
|
||||
.requiredSize(IconRequiredSize),
|
||||
.padding(end = CardDefaults.IconSpacing)
|
||||
.requiredSize(CardDefaults.IconRequiredSize),
|
||||
painter = painterResource(R.drawable.ic_error_outline),
|
||||
contentDescription = null,
|
||||
) }) else null,
|
||||
@@ -135,7 +144,7 @@ fun FlorisWarningCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showIcon: Boolean = true,
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
FlorisSimpleCard(
|
||||
@@ -145,8 +154,8 @@ fun FlorisWarningCard(
|
||||
onClick = onClick,
|
||||
icon = if (showIcon) ({ Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = IconEndPadding)
|
||||
.requiredSize(IconRequiredSize),
|
||||
.padding(end = CardDefaults.IconSpacing)
|
||||
.requiredSize(CardDefaults.IconRequiredSize),
|
||||
painter = painterResource(R.drawable.ic_warning_outline),
|
||||
contentDescription = null,
|
||||
) }) else null,
|
||||
@@ -160,7 +169,7 @@ fun FlorisInfoCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showIcon: Boolean = true,
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
FlorisSimpleCard(
|
||||
@@ -168,8 +177,8 @@ fun FlorisInfoCard(
|
||||
onClick = onClick,
|
||||
icon = if (showIcon) ({ Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = IconEndPadding)
|
||||
.requiredSize(IconRequiredSize),
|
||||
.padding(end = CardDefaults.IconSpacing)
|
||||
.requiredSize(CardDefaults.IconRequiredSize),
|
||||
painter = painterResource(R.drawable.ic_info),
|
||||
contentDescription = null,
|
||||
) }) else null,
|
||||
@@ -186,9 +195,10 @@ fun FlorisOutlinedBox(
|
||||
subtitle: String? = null,
|
||||
onSubtitleClick: (() -> Unit)? = null,
|
||||
borderWidth: Dp = 1.dp,
|
||||
borderColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||
shape: Shape = OutlinedBoxShape,
|
||||
content: @Composable () -> Unit,
|
||||
borderColor: Color = MaterialTheme.colors.outline,
|
||||
shape: Shape = BoxDefaults.OutlinedBoxShape,
|
||||
contentPadding: PaddingValues = BoxDefaults.ContentPadding,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = modifier,
|
||||
@@ -220,10 +230,13 @@ fun FlorisOutlinedBox(
|
||||
borderWidth = borderWidth,
|
||||
borderColor = borderColor,
|
||||
shape = shape,
|
||||
contentPadding = contentPadding,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Rework internal implementation (with same API and visual appearance) of FlorisOutlinedBox
|
||||
// to avoid too much nesting and improve performance
|
||||
@Composable
|
||||
fun FlorisOutlinedBox(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -232,9 +245,10 @@ fun FlorisOutlinedBox(
|
||||
subtitle: (@Composable () -> Unit)? = null,
|
||||
onSubtitleClick: (() -> Unit)? = null,
|
||||
borderWidth: Dp = 1.dp,
|
||||
borderColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||
shape: Shape = OutlinedBoxShape,
|
||||
content: @Composable () -> Unit,
|
||||
borderColor: Color = MaterialTheme.colors.outline,
|
||||
shape: Shape = BoxDefaults.OutlinedBoxShape,
|
||||
contentPadding: PaddingValues = BoxDefaults.ContentPadding,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -257,7 +271,12 @@ fun FlorisOutlinedBox(
|
||||
subtitle()
|
||||
}
|
||||
}
|
||||
content()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
if (title != null) {
|
||||
Box(
|
||||
@@ -276,3 +295,9 @@ fun FlorisOutlinedBox(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.defaultFlorisOutlinedBox(): Modifier {
|
||||
return this
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -43,16 +44,19 @@ fun FlorisChip(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = { },
|
||||
enabled: Boolean = true,
|
||||
color: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity),
|
||||
color: Color = Color.Unspecified,
|
||||
shape: Shape = CircleShape,
|
||||
@DrawableRes leadingIcons: List<Int> = listOf(),
|
||||
@DrawableRes trailingIcons: List<Int> = listOf(),
|
||||
) {
|
||||
val backgroundColor = color.takeOrElse {
|
||||
MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity)
|
||||
}
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
color = color,
|
||||
color = backgroundColor,
|
||||
shape = shape,
|
||||
) {
|
||||
Row(
|
||||
|
||||
@@ -31,6 +31,7 @@ fun FlorisConfirmDeleteDialog(
|
||||
what: String,
|
||||
) {
|
||||
JetPrefAlertDialog(
|
||||
modifier = modifier,
|
||||
title = stringRes(R.string.action__delete_confirm_title),
|
||||
confirmLabel = stringRes(R.string.action__delete),
|
||||
onConfirm = onConfirm,
|
||||
@@ -40,3 +41,25 @@ fun FlorisConfirmDeleteDialog(
|
||||
Text(text = stringRes(R.string.action__delete_confirm_message, "name" to what))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisUnsavedChangesDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onSave: () -> Unit,
|
||||
onDiscard: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
JetPrefAlertDialog(
|
||||
modifier = modifier,
|
||||
title = stringRes(R.string.action__discard_confirm_title),
|
||||
confirmLabel = stringRes(R.string.action__save),
|
||||
onConfirm = onSave,
|
||||
dismissLabel = stringRes(R.string.action__discard),
|
||||
onDismiss = onDiscard,
|
||||
onOutsideDismissal = onDismiss,
|
||||
neutralLabel = stringRes(R.string.action__cancel),
|
||||
onNeutral = onDismiss,
|
||||
) {
|
||||
Text(text = stringRes(R.string.action__discard_confirm_message))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,43 +35,55 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
@Composable
|
||||
fun FlorisDropdownMenu(
|
||||
items: List<String>,
|
||||
fun <T : Any> FlorisDropdownMenu(
|
||||
items: List<T>,
|
||||
expanded: Boolean,
|
||||
selectedIndex: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
isError: Boolean = false,
|
||||
labelProvider: (@Composable (T) -> String)? = null,
|
||||
onSelectItem: (Int) -> Unit = { },
|
||||
onExpandRequest: () -> Unit = { },
|
||||
onDismissRequest: () -> Unit = { },
|
||||
) {
|
||||
@Composable
|
||||
fun asString(v: T): String {
|
||||
return labelProvider?.invoke(v) ?: v.toString()
|
||||
}
|
||||
|
||||
Box(modifier = modifier.wrapContentSize(Alignment.TopStart)) {
|
||||
val indicatorRotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f)
|
||||
val index = selectedIndex.coerceIn(items.indices)
|
||||
val color = if (isError) {
|
||||
val color = if (!enabled) {
|
||||
MaterialTheme.colors.outline
|
||||
} else if (isError) {
|
||||
MaterialTheme.colors.error
|
||||
} else {
|
||||
MaterialTheme.colors.onBackground
|
||||
}
|
||||
OutlinedButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
border = if (isError) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
border = if (isError && enabled) {
|
||||
BorderStroke(ButtonDefaults.OutlinedBorderSize, MaterialTheme.colors.error)
|
||||
} else {
|
||||
ButtonDefaults.outlinedBorder
|
||||
},
|
||||
onClick = { onExpandRequest() },
|
||||
enabled = enabled,
|
||||
onClick = onExpandRequest,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
text = items[index],
|
||||
text = asString(items[index]),
|
||||
textAlign = TextAlign.Start,
|
||||
fontWeight = FontWeight.Normal,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
color = color,
|
||||
@@ -79,7 +91,11 @@ fun FlorisDropdownMenu(
|
||||
Icon(
|
||||
modifier = Modifier.rotate(indicatorRotation),
|
||||
painter = painterResource(R.drawable.ic_keyboard_arrow_down),
|
||||
tint = color.copy(alpha = ContentAlpha.medium),
|
||||
tint = if (enabled) {
|
||||
color.copy(alpha = ContentAlpha.medium)
|
||||
} else {
|
||||
color
|
||||
},
|
||||
contentDescription = "Dropdown indicator",
|
||||
)
|
||||
}
|
||||
@@ -94,7 +110,7 @@ fun FlorisDropdownMenu(
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = item)
|
||||
Text(text = asString(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +144,7 @@ fun FlorisDropdownLikeButton(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
text = item,
|
||||
textAlign = TextAlign.Start,
|
||||
fontWeight = FontWeight.Normal,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
color = color,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -28,6 +27,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.prefs.AppPrefs
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceLayout
|
||||
@@ -44,11 +46,12 @@ typealias FlorisScreenActions = @Composable RowScope.() -> Unit
|
||||
typealias FlorisScreenBottomBar = @Composable () -> Unit
|
||||
typealias FlorisScreenContent = PreferenceUiContent<AppPrefs>
|
||||
typealias FlorisScreenFab = @Composable () -> Unit
|
||||
typealias FlorisScreenNavigationIcon = @Composable () -> Unit
|
||||
|
||||
interface FlorisScreenScope {
|
||||
var title: String
|
||||
|
||||
var backArrowVisible: Boolean
|
||||
var navigationIconVisible: Boolean
|
||||
|
||||
var previewFieldVisible: Boolean
|
||||
|
||||
@@ -60,22 +63,31 @@ interface FlorisScreenScope {
|
||||
|
||||
fun bottomBar(bottomBar: FlorisScreenBottomBar)
|
||||
|
||||
fun content(content: FlorisScreenContent)
|
||||
|
||||
fun floatingActionButton(fab: FlorisScreenFab)
|
||||
|
||||
fun content(content: FlorisScreenContent)
|
||||
fun navigationIcon(navigationIcon: FlorisScreenNavigationIcon)
|
||||
}
|
||||
|
||||
private class FlorisScreenScopeImpl : FlorisScreenScope {
|
||||
override var title: String by mutableStateOf("")
|
||||
override var backArrowVisible: Boolean by mutableStateOf(true)
|
||||
override var navigationIconVisible: Boolean by mutableStateOf(true)
|
||||
override var previewFieldVisible: Boolean by mutableStateOf(false)
|
||||
override var scrollable: Boolean by mutableStateOf(true)
|
||||
override var iconSpaceReserved: Boolean by mutableStateOf(true)
|
||||
|
||||
private var actions: FlorisScreenActions = { }
|
||||
private var bottomBar: FlorisScreenBottomBar = { }
|
||||
private var content: FlorisScreenContent = { }
|
||||
private var fab: FlorisScreenFab = { }
|
||||
private var actions: FlorisScreenActions = @Composable { }
|
||||
private var bottomBar: FlorisScreenBottomBar = @Composable { }
|
||||
private var content: FlorisScreenContent = @Composable { }
|
||||
private var fab: FlorisScreenFab = @Composable { }
|
||||
private var navigationIcon: FlorisScreenNavigationIcon = @Composable {
|
||||
val navController = LocalNavController.current
|
||||
FlorisIconButton(
|
||||
onClick = { navController.popBackStack() },
|
||||
icon = painterResource(R.drawable.ic_arrow_back),
|
||||
)
|
||||
}
|
||||
|
||||
override fun actions(actions: FlorisScreenActions) {
|
||||
this.actions = actions
|
||||
@@ -93,6 +105,10 @@ private class FlorisScreenScopeImpl : FlorisScreenScope {
|
||||
this.fab = fab
|
||||
}
|
||||
|
||||
override fun navigationIcon(navigationIcon: FlorisScreenNavigationIcon) {
|
||||
this.navigationIcon = navigationIcon
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Render() {
|
||||
val previewFieldController = LocalPreviewFieldController.current
|
||||
@@ -102,7 +118,7 @@ private class FlorisScreenScopeImpl : FlorisScreenScope {
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { FlorisAppBar(title, backArrowVisible, actions) },
|
||||
topBar = { FlorisAppBar(title, navigationIcon.takeIf { navigationIconVisible }, actions) },
|
||||
bottomBar = bottomBar,
|
||||
floatingActionButton = fab,
|
||||
) { innerPadding ->
|
||||
@@ -110,15 +126,15 @@ private class FlorisScreenScopeImpl : FlorisScreenScope {
|
||||
Modifier.florisVerticalScroll()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier = modifier.padding(innerPadding)) {
|
||||
PreferenceLayout(
|
||||
florisPreferenceModel(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
iconSpaceReserved = iconSpaceReserved,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
PreferenceLayout(
|
||||
florisPreferenceModel(),
|
||||
modifier = modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
iconSpaceReserved = iconSpaceReserved,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
private val StepHeaderPaddingVertical = 16.dp
|
||||
private val StepHeaderNumberBoxSize = 40.dp
|
||||
@@ -198,7 +199,7 @@ private fun ColumnScope.Step(
|
||||
val autoStepId by stepState.getCurrentAuto()
|
||||
val backgroundColor = when (ownStepId) {
|
||||
currentStepId -> primaryColor
|
||||
else -> MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
|
||||
else -> MaterialTheme.colors.outline
|
||||
}
|
||||
val contentVisible = ownStepId == currentStepId
|
||||
StepHeader(
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldColors
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
import dev.patrickgold.florisboard.common.ValidationResult
|
||||
|
||||
@Composable
|
||||
fun FlorisOutlinedTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
placeholder: String? = null,
|
||||
isError: Boolean = false,
|
||||
showValidationHint: Boolean = true,
|
||||
showValidationError: Boolean = false,
|
||||
validationResult: ValidationResult? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = MaterialTheme.shapes.small,
|
||||
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||
) {
|
||||
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
|
||||
val textFieldValue = textFieldValueState.copy(text = value)
|
||||
|
||||
FlorisOutlinedTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = {
|
||||
textFieldValueState = it
|
||||
if (value != it.text) {
|
||||
onValueChange(it.text)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
placeholder = placeholder,
|
||||
isError = isError,
|
||||
showValidationHint = showValidationHint,
|
||||
showValidationError = showValidationError,
|
||||
validationResult = validationResult,
|
||||
visualTransformation = visualTransformation,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisOutlinedTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = MaterialTheme.typography.button,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
placeholder: String? = null,
|
||||
isError: Boolean = false,
|
||||
showValidationHint: Boolean = true,
|
||||
showValidationError: Boolean = false,
|
||||
validationResult: ValidationResult? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = MaterialTheme.shapes.small,
|
||||
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
unfocusedBorderColor = MaterialTheme.colors.outline,
|
||||
disabledBorderColor = MaterialTheme.colors.outline,
|
||||
),
|
||||
) {
|
||||
val textColor = textStyle.color.takeOrElse {
|
||||
colors.textColor(enabled).value
|
||||
}
|
||||
val mergedTextStyle = textStyle.copy(color = textColor)
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
val isErrorState = isError || (showValidationError && validationResult?.isInvalid() == true)
|
||||
|
||||
BasicTextField(
|
||||
modifier = modifier.padding(vertical = 4.dp),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
visualTransformation = visualTransformation,
|
||||
cursorBrush = SolidColor(colors.cursorColor(isErrorState).value),
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = colors.backgroundColor(enabled).value,
|
||||
border = if (isErrorState && enabled) {
|
||||
BorderStroke(ButtonDefaults.OutlinedBorderSize, MaterialTheme.colors.error)
|
||||
} else if (isFocused) {
|
||||
BorderStroke(ButtonDefaults.OutlinedBorderSize, MaterialTheme.colors.primary)
|
||||
} else {
|
||||
ButtonDefaults.outlinedBorder
|
||||
},
|
||||
shape = shape,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(
|
||||
minWidth = ButtonDefaults.MinWidth,
|
||||
minHeight = 40.dp,
|
||||
)
|
||||
.padding(ButtonDefaults.ContentPadding),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
ProvideTextStyle(value = mergedTextStyle) {
|
||||
innerTextField()
|
||||
}
|
||||
if (!placeholder.isNullOrBlank()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.56f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (showValidationHint && validationResult?.isValid() == true && validationResult.hasHintMessage()) {
|
||||
Text(
|
||||
text = validationResult.hintMessage(),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.56f),
|
||||
)
|
||||
}
|
||||
|
||||
if (showValidationError && validationResult?.isInvalid() == true && validationResult.hasErrorMessage()) {
|
||||
Text(
|
||||
text = validationResult.errorMessage(),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.ButtonDefaults
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisTextButton
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
|
||||
@Composable
|
||||
fun ExtensionComponentNoneFoundView() {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__components_none_found),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionComponentView(
|
||||
meta: ExtensionMeta,
|
||||
component: ExtensionComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
onDeleteBtnClick: (() -> Unit)? = null,
|
||||
onEditBtnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val componentName = remember(meta.id, component.id) { ExtensionComponentName(meta.id, component.id).toString() }
|
||||
FlorisOutlinedBox(
|
||||
modifier = modifier,
|
||||
title = component.label,
|
||||
subtitle = componentName,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = if (onDeleteBtnClick == null && onEditBtnClick == null) 8.dp else 0.dp,
|
||||
),
|
||||
) {
|
||||
when (component) {
|
||||
is ThemeExtensionComponent -> {
|
||||
val text = remember(
|
||||
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()}")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
|
||||
)
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
if (onDeleteBtnClick != null || onEditBtnClick != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp),
|
||||
) {
|
||||
if (onDeleteBtnClick != null) {
|
||||
FlorisTextButton(
|
||||
onClick = onDeleteBtnClick,
|
||||
icon = painterResource(R.drawable.ic_delete),
|
||||
text = stringRes(R.string.action__delete),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colors.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (onEditBtnClick != null) {
|
||||
FlorisTextButton(
|
||||
onClick = onEditBtnClick,
|
||||
icon = painterResource(R.drawable.ic_edit),
|
||||
text = stringRes(R.string.action__edit),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun <T : ExtensionComponent> ExtensionComponentListView(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
components: List<T>,
|
||||
onCreateBtnClick: (() -> Unit)? = null,
|
||||
componentGenerator: @Composable (T) -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ListItem(
|
||||
text = { Text(
|
||||
text = title,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
) },
|
||||
trailing = if (onCreateBtnClick != null) {
|
||||
@Composable {
|
||||
FlorisIconButton(
|
||||
onClick = onCreateBtnClick,
|
||||
icon = painterResource(R.drawable.ic_add),
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
} else { null },
|
||||
)
|
||||
if (components.isEmpty()) {
|
||||
ExtensionComponentNoneFoundView()
|
||||
} else {
|
||||
for (component in components) {
|
||||
componentGenerator(component)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,898 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisButtonBar
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisInfoCard
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedTextField
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisUnsavedChangesDialog
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.RadioListItem
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.DialogProperty
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.ThemeEditorScreen
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.common.ValidationResult
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.common.rememberValidationResult
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentEditor
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentImpl
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.FlorisRef
|
||||
import dev.patrickgold.florisboard.res.ZipUtils
|
||||
import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionDefaults
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionJsonConfig
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMaintainer
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionManager
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionValidation
|
||||
import dev.patrickgold.florisboard.res.ext.validate
|
||||
import dev.patrickgold.florisboard.res.io.subFile
|
||||
import dev.patrickgold.florisboard.res.io.writeJson
|
||||
import dev.patrickgold.florisboard.snygg.SnyggStylesheetJsonConfig
|
||||
import dev.patrickgold.florisboard.themeManager
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private val TextFieldVerticalPadding = 8.dp
|
||||
private val MetaDataContentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp)
|
||||
|
||||
private const val AnimationDuration = 300
|
||||
|
||||
private val ActionScreenEnterTransition = fadeIn(tween(AnimationDuration))
|
||||
private val ActionScreenExitTransition = fadeOut(tween(AnimationDuration))
|
||||
|
||||
sealed class EditorAction {
|
||||
object ManageMetaData : EditorAction()
|
||||
|
||||
object ManageDependencies : EditorAction()
|
||||
|
||||
object ManageFiles : EditorAction()
|
||||
|
||||
data class CreateComponent<T : ExtensionComponent>(val type: KClass<T>) : EditorAction()
|
||||
|
||||
data class ManageComponent(val editor: ExtensionComponent) : EditorAction()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionEditScreen(id: String, createSerialType: String?) {
|
||||
val context = LocalContext.current
|
||||
val cacheManager by context.cacheManager()
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
@Suppress("unchecked_cast")
|
||||
fun <W : CacheManager.ExtEditorWorkspace<T>, T : ExtensionEditor> getOrCreateWorkspace(
|
||||
uuid: String,
|
||||
container: CacheManager.WorkspacesContainer<W>,
|
||||
ext: Extension,
|
||||
): W {
|
||||
val workspace = container.getWorkspaceByUuid(uuid)
|
||||
return workspace ?: container.new(uuid).also { newWorkspace ->
|
||||
val sourceRef = ext.sourceRef
|
||||
if (createSerialType == null) {
|
||||
checkNotNull(sourceRef) { "Extension source ref must not be null" }
|
||||
ZipUtils.unzip(context, sourceRef, newWorkspace.extDir)
|
||||
}
|
||||
newWorkspace.ext = ext
|
||||
newWorkspace.editor = ext.edit() as? T
|
||||
}
|
||||
}
|
||||
|
||||
val ext = extensionManager.getExtensionById(id) ?: remember {
|
||||
val meta = ExtensionMeta(
|
||||
id = ExtensionDefaults.createLocalId("themes", System.currentTimeMillis().toString()),
|
||||
version = "0.0.0",
|
||||
title = "My themes",
|
||||
maintainers = listOf(ExtensionMaintainer(name = "Local")),
|
||||
license = "(none specified)",
|
||||
)
|
||||
when (createSerialType) {
|
||||
ThemeExtension.SERIAL_TYPE -> ThemeExtension(meta, null, emptyList())
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (ext != null) {
|
||||
val uuid = rememberSaveable { UUID.randomUUID().toString() }
|
||||
val cacheWorkspace = remember {
|
||||
runCatching {
|
||||
when (ext) {
|
||||
is ThemeExtension -> {
|
||||
getOrCreateWorkspace(uuid, cacheManager.themeExtEditor, ext)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
cacheWorkspace.onSuccess { workspace ->
|
||||
if (workspace?.editor != null) {
|
||||
ExtensionEditScreenSheetSwitcher(workspace, isCreateExt = createSerialType != null)
|
||||
} else {
|
||||
ExtensionNotFoundScreen(id = id)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
Text(text = remember(error) { error.stackTraceToString() })
|
||||
}
|
||||
} else {
|
||||
ExtensionNotFoundScreen(id)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionEditScreenSheetSwitcher(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
isCreateExt: Boolean,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
EditScreen(workspace, isCreateExt)
|
||||
AnimatedVisibility(
|
||||
visible = workspace.currentAction != null,
|
||||
enter = ActionScreenEnterTransition,
|
||||
exit = ActionScreenExitTransition,
|
||||
) {
|
||||
when (val action = workspace.currentAction) {
|
||||
is EditorAction.ManageMetaData -> {
|
||||
ManageMetaDataScreen(workspace, isCreateExt)
|
||||
}
|
||||
is EditorAction.ManageDependencies -> {
|
||||
ManageDependenciesScreen(workspace)
|
||||
}
|
||||
is EditorAction.ManageFiles -> {
|
||||
ManageFilesScreen(workspace)
|
||||
}
|
||||
is EditorAction.CreateComponent<*> -> {
|
||||
CreateComponentScreen(workspace, action.type)
|
||||
}
|
||||
is EditorAction.ManageComponent -> when (action.editor) {
|
||||
is ThemeExtensionComponentEditor -> {
|
||||
ThemeEditorScreen(workspace, action.editor)
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun EditScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
isCreateExt: Boolean,
|
||||
) = FlorisScreen {
|
||||
title = stringRes(if (isCreateExt) {
|
||||
when (workspace.ext) {
|
||||
is KeyboardExtension -> R.string.ext__editor__title_create_keyboard
|
||||
is SpellingExtension -> R.string.ext__editor__title_create_spelling
|
||||
is ThemeExtension -> R.string.ext__editor__title_create_theme
|
||||
else -> R.string.ext__editor__title_create_any
|
||||
}
|
||||
} else {
|
||||
when (workspace.ext) {
|
||||
is KeyboardExtension -> R.string.ext__editor__title_edit_keyboard
|
||||
is SpellingExtension -> R.string.ext__editor__title_edit_spelling
|
||||
is ThemeExtension -> R.string.ext__editor__title_edit_theme
|
||||
else -> R.string.ext__editor__title_edit_any
|
||||
}
|
||||
})
|
||||
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
val extEditor = workspace.editor ?: return@FlorisScreen
|
||||
var showUnsavedChangesDialog by remember { mutableStateOf(false) }
|
||||
var showInvalidMetadataDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun handleBackPress() {
|
||||
if (workspace.isModified) {
|
||||
showUnsavedChangesDialog = true
|
||||
} else {
|
||||
workspace.close()
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSave() {
|
||||
if (!extEditor.meta.validate()) {
|
||||
showUnsavedChangesDialog = false
|
||||
showInvalidMetadataDialog = true
|
||||
return
|
||||
}
|
||||
val manifest = extEditor.build()
|
||||
val manifestFile = workspace.saverDir.subFile(ExtensionDefaults.MANIFEST_FILE_NAME)
|
||||
manifestFile.writeJson(manifest, ExtensionJsonConfig)
|
||||
when (extEditor) {
|
||||
is ThemeExtensionEditor -> {
|
||||
for (theme in extEditor.themes) {
|
||||
val stylesheetFile = workspace.saverDir.subFile(theme.stylesheetPath())
|
||||
stylesheetFile.parentFile?.mkdirs()
|
||||
val stylesheetEditor = theme.stylesheetEditor
|
||||
if (stylesheetEditor != null) {
|
||||
val stylesheet = stylesheetEditor.build()
|
||||
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
|
||||
} else {
|
||||
val unmodifiedStylesheetFile = workspace.extDir.subFile(theme.stylesheetPath())
|
||||
if (unmodifiedStylesheetFile.exists()) {
|
||||
unmodifiedStylesheetFile.copyTo(stylesheetFile, overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
val flexArchiveName = ExtensionDefaults.createFlexName(extEditor.meta.id)
|
||||
val flexArchiveFile = workspace.dir.subFile(flexArchiveName)
|
||||
ZipUtils.zip(workspace.saverDir, flexArchiveFile)
|
||||
val sourceRef = if (isCreateExt) {
|
||||
FlorisRef.internal(ExtensionManager.IME_THEME_PATH).subRef(flexArchiveName)
|
||||
} else {
|
||||
workspace.ext!!.sourceRef!!
|
||||
}
|
||||
flexArchiveFile.copyTo(sourceRef.absoluteFile(context), overwrite = true)
|
||||
workspace.close()
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_arrow_back),
|
||||
)
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(text = stringRes(R.string.action__cancel)) {
|
||||
handleBackPress()
|
||||
}
|
||||
ButtonBarButton(text = stringRes(R.string.action__save)) {
|
||||
handleSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
) {
|
||||
this@content.Preference(
|
||||
onClick = { workspace.currentAction = EditorAction.ManageMetaData },
|
||||
iconId = R.drawable.ic_code,
|
||||
title = stringRes(R.string.ext__editor__metadata__title),
|
||||
)
|
||||
this@content.Preference(
|
||||
onClick = { workspace.currentAction = EditorAction.ManageDependencies },
|
||||
iconId = R.drawable.ic_library_books,
|
||||
title = stringRes(R.string.ext__editor__dependencies__title),
|
||||
)
|
||||
this@content.Preference(
|
||||
onClick = { workspace.currentAction = EditorAction.ManageFiles },
|
||||
iconId = R.drawable.ic_file_blank,
|
||||
title = stringRes(R.string.ext__editor__files__title),
|
||||
)
|
||||
}
|
||||
|
||||
when (extEditor) {
|
||||
is ThemeExtensionEditor -> {
|
||||
ExtensionComponentListView(
|
||||
title = stringRes(R.string.ext__meta__components_theme),
|
||||
components = extEditor.themes,
|
||||
onCreateBtnClick = {
|
||||
workspace.currentAction = EditorAction.CreateComponent(ThemeExtensionComponent::class)
|
||||
},
|
||||
) { component ->
|
||||
ExtensionComponentView(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
meta = extEditor.meta,
|
||||
component = component,
|
||||
onDeleteBtnClick = { workspace.update { extEditor.themes.remove(component) } },
|
||||
onEditBtnClick = { workspace.currentAction = EditorAction.ManageComponent(component) },
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (showUnsavedChangesDialog) {
|
||||
FlorisUnsavedChangesDialog(
|
||||
onSave = {
|
||||
handleSave()
|
||||
},
|
||||
onDiscard = {
|
||||
navController.popBackStack()
|
||||
showUnsavedChangesDialog = false
|
||||
},
|
||||
onDismiss = {
|
||||
showUnsavedChangesDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showInvalidMetadataDialog) {
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(R.string.ext__editor__metadata__title_invalid),
|
||||
confirmLabel = stringRes(R.string.action__ok),
|
||||
onConfirm = {
|
||||
showInvalidMetadataDialog = false
|
||||
},
|
||||
onDismiss = {
|
||||
showInvalidMetadataDialog = false
|
||||
},
|
||||
content = {
|
||||
Text(text = stringRes(R.string.ext__editor__metadata__message_invalid))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageMetaDataScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
isCreateExt: Boolean,
|
||||
) = FlorisScreen {
|
||||
title = stringRes(R.string.ext__editor__metadata__title)
|
||||
|
||||
val meta = workspace.editor?.meta ?: return@FlorisScreen
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var id by rememberSaveable { mutableStateOf(meta.id) }
|
||||
val idValidation = rememberValidationResult(ExtensionValidation.MetaId, id)
|
||||
var version by rememberSaveable { mutableStateOf(meta.version) }
|
||||
val versionValidation = rememberValidationResult(ExtensionValidation.MetaVersion, version)
|
||||
var title by rememberSaveable { mutableStateOf(meta.title) }
|
||||
val titleValidation = rememberValidationResult(ExtensionValidation.MetaTitle, title)
|
||||
var description by rememberSaveable { mutableStateOf(meta.description ?: "") }
|
||||
var keywords by rememberSaveable { mutableStateOf(meta.keywords?.joinToString("\n") ?: "") }
|
||||
var homepage by rememberSaveable { mutableStateOf(meta.homepage ?: "") }
|
||||
var issueTracker by rememberSaveable { mutableStateOf(meta.issueTracker ?: "") }
|
||||
var maintainers by rememberSaveable { mutableStateOf(meta.maintainers.joinToString("\n")) }
|
||||
val maintainersValidation = rememberValidationResult(ExtensionValidation.MetaMaintainers, maintainers)
|
||||
var license by rememberSaveable { mutableStateOf(meta.license) }
|
||||
val licenseValidation = rememberValidationResult(ExtensionValidation.MetaLicense, license)
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
fun handleApply() {
|
||||
val invalid = idValidation.isInvalid() ||
|
||||
versionValidation.isInvalid() ||
|
||||
titleValidation.isInvalid() ||
|
||||
maintainersValidation.isInvalid() ||
|
||||
licenseValidation.isInvalid()
|
||||
if (invalid) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
workspace.update {
|
||||
workspace.editor?.meta = ExtensionMeta(
|
||||
id = id.trim(),
|
||||
version = version.trim(),
|
||||
title = title.trim(),
|
||||
description = description.trim().takeIf { it.isNotBlank() },
|
||||
keywords = keywords.lines().map { it.trim() }.filter { it.isNotBlank() }.takeIf { it.isNotEmpty() },
|
||||
homepage = homepage.trim().takeIf { it.isNotBlank() },
|
||||
issueTracker = issueTracker.trim().takeIf { it.isNotBlank() },
|
||||
maintainers = maintainers.lines().map { it.trim() }.filter { it.isNotBlank() }
|
||||
.map { ExtensionMaintainer.fromOrTakeRaw(it) },
|
||||
license = license.trim(),
|
||||
)
|
||||
}
|
||||
workspace.currentAction = null
|
||||
}
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(text = stringRes(R.string.action__cancel)) {
|
||||
handleBackPress()
|
||||
}
|
||||
ButtonBarButton(text = stringRes(R.string.action__apply)) {
|
||||
handleApply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(MetaDataContentPadding)) {
|
||||
EditorSheetTextField(
|
||||
enabled = isCreateExt,
|
||||
isRequired = true,
|
||||
value = id,
|
||||
onValueChange = { id = it },
|
||||
label = stringRes(R.string.ext__meta__id),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = idValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = version,
|
||||
onValueChange = { version = it },
|
||||
label = stringRes(R.string.ext__meta__version),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = versionValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = stringRes(R.string.ext__meta__title),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = titleValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = stringRes(R.string.ext__meta__description),
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = keywords,
|
||||
onValueChange = { keywords = it },
|
||||
label = stringRes(R.string.ext__meta__keywords),
|
||||
singleLine = false,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = homepage,
|
||||
onValueChange = { homepage = it },
|
||||
label = stringRes(R.string.ext__meta__homepage),
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = issueTracker,
|
||||
onValueChange = { issueTracker = it },
|
||||
label = stringRes(R.string.ext__meta__issue_tracker),
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = maintainers,
|
||||
onValueChange = { maintainers = it },
|
||||
label = stringRes(R.string.ext__meta__maintainers),
|
||||
singleLine = false,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = maintainersValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = license,
|
||||
onValueChange = { license = it },
|
||||
label = stringRes(R.string.ext__meta__license),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = licenseValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageDependenciesScreen(workspace: CacheManager.ExtEditorWorkspace<*>) = FlorisScreen {
|
||||
title = stringRes(R.string.ext__editor__dependencies__title)
|
||||
|
||||
val dependencyList = workspace.editor?.dependencies ?: return@FlorisScreen
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
text = """
|
||||
Dependencies are currently not implemented, but are already somewhat
|
||||
integrated as a placeholder for the future.
|
||||
""".trimIndent().replace('\n', ' '),
|
||||
)
|
||||
if (dependencyList.isEmpty()) {
|
||||
Text(text = "no deps found")
|
||||
} else {
|
||||
for (dependency in dependencyList) {
|
||||
Text(text = dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 = painterResource(R.drawable.ic_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;
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : ExtensionComponent> CreateComponentScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
type: KClass<T>,
|
||||
) = FlorisScreen {
|
||||
title = stringRes(when (type) {
|
||||
ThemeExtensionComponent::class -> R.string.ext__editor__create_component__title_theme
|
||||
else -> R.string.ext__editor__create_component__title
|
||||
})
|
||||
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
val themeManager by context.themeManager()
|
||||
|
||||
var createFrom by rememberSaveable { mutableStateOf(CreateFrom.EXISTING) }
|
||||
val extId = workspace.editor?.meta?.id ?: "null"
|
||||
val components = remember<Map<ExtensionComponentName, ExtensionComponent>> {
|
||||
when (val editor = workspace.editor) {
|
||||
is ThemeExtensionEditor -> buildMap {
|
||||
for (theme in editor.themes) {
|
||||
put(ExtensionComponentName(extId, theme.id), theme)
|
||||
}
|
||||
for ((componentName, theme) in themeManager.indexedThemeConfigs.value ?: emptyMap()) {
|
||||
if (componentName.extensionId != extId) {
|
||||
put(componentName, theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
var selectedComponentName by rememberSaveable(stateSaver = ExtensionComponentName.Saver) {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var newId by rememberSaveable { mutableStateOf("") }
|
||||
val newIdValidation = rememberValidationResult(ExtensionValidation.ComponentId, newId)
|
||||
var newLabel by rememberSaveable { mutableStateOf("") }
|
||||
val newLabelValidation = rememberValidationResult(ExtensionValidation.ComponentLabel, newLabel)
|
||||
var newAuthors by rememberSaveable { mutableStateOf("") }
|
||||
val newAuthorsValidation = rememberValidationResult(ExtensionValidation.ComponentAuthors, newAuthors)
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
fun handleCreate() {
|
||||
val invalid = createFrom == CreateFrom.EMPTY && (newIdValidation.isInvalid() ||
|
||||
newLabelValidation.isInvalid() || newAuthorsValidation.isInvalid())
|
||||
if (invalid) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
when (val editor = workspace.editor) {
|
||||
is ThemeExtensionEditor -> {
|
||||
when (createFrom) {
|
||||
CreateFrom.EMPTY -> {
|
||||
if (editor.themes.any { it.id == newId.trim() }) {
|
||||
context.showLongToast("A theme with this ID already exists!")
|
||||
} else {
|
||||
val componentEditor = ThemeExtensionComponentEditor(
|
||||
id = newId.trim(),
|
||||
label = newLabel.trim(),
|
||||
authors = newAuthors.lines().map { it.trim() }.filter { it.isNotBlank() },
|
||||
)
|
||||
editor.themes.add(componentEditor)
|
||||
workspace.currentAction = null
|
||||
}
|
||||
}
|
||||
CreateFrom.EXISTING -> {
|
||||
val componentName = selectedComponentName ?: return
|
||||
val componentId = if (editor.themes.any { it.id == componentName.componentId }) {
|
||||
var suffix = 1
|
||||
var tempId: String
|
||||
do {
|
||||
tempId = "${componentName.componentId}_${suffix++}"
|
||||
} while (editor.themes.any { it.id == tempId })
|
||||
tempId
|
||||
} else {
|
||||
componentName.componentId
|
||||
}
|
||||
if (componentName.extensionId == extId) {
|
||||
val component = editor.themes.find { it.id == componentName.componentId } ?: return
|
||||
val componentEditor = component.let { c ->
|
||||
ThemeExtensionComponentEditor(
|
||||
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()
|
||||
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
|
||||
componentEditor.stylesheetEditor = null
|
||||
} else {
|
||||
val srcStylesheetFile = workspace.extDir.subFile(component.stylesheetPath())
|
||||
val dstStylesheetFile = workspace.extDir.subFile(componentEditor.stylesheetPath())
|
||||
dstStylesheetFile.parentFile?.mkdirs()
|
||||
srcStylesheetFile.copyTo(dstStylesheetFile, overwrite = true)
|
||||
}
|
||||
editor.themes.add(componentEditor)
|
||||
} else {
|
||||
val component = themeManager.indexedThemeConfigs.value?.get(componentName) ?: return
|
||||
val componentEditor = (component as? ThemeExtensionComponentImpl)?.edit() ?: return
|
||||
componentEditor.id = componentId
|
||||
componentEditor.stylesheetPath = ""
|
||||
val externalExt = extensionManager.getExtensionById(componentName.extensionId) ?: return
|
||||
val stylesheetJson = ZipUtils.readFileFromArchive(
|
||||
context, externalExt.sourceRef!!, component.stylesheetPath()
|
||||
).getOrNull() ?: return
|
||||
val dstStylesheetFile = workspace.extDir.subFile(componentEditor.stylesheetPath())
|
||||
dstStylesheetFile.parentFile?.mkdirs()
|
||||
dstStylesheetFile.writeText(stylesheetJson)
|
||||
editor.themes.add(componentEditor)
|
||||
}
|
||||
workspace.currentAction = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasSufficientInfoForCreating(): Boolean {
|
||||
return when (createFrom) {
|
||||
CreateFrom.EMPTY -> newId.isNotBlank() && newLabel.isNotBlank() && newAuthors.isNotBlank()
|
||||
CreateFrom.EXISTING -> components.containsKey(selectedComponentName)
|
||||
}
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(text = stringRes(R.string.action__cancel)) {
|
||||
handleBackPress()
|
||||
}
|
||||
ButtonBarButton(
|
||||
text = stringRes(R.string.action__create),
|
||||
enabled = hasSufficientInfoForCreating(),
|
||||
) {
|
||||
handleCreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
) {
|
||||
RadioListItem(
|
||||
onClick = { createFrom = CreateFrom.EXISTING },
|
||||
selected = createFrom == CreateFrom.EXISTING,
|
||||
text = stringRes(R.string.ext__editor__create_component__from_existing),
|
||||
)
|
||||
RadioListItem(
|
||||
onClick = { createFrom = CreateFrom.EMPTY },
|
||||
selected = createFrom == CreateFrom.EMPTY,
|
||||
text = stringRes(R.string.ext__editor__create_component__from_empty),
|
||||
)
|
||||
}
|
||||
|
||||
if (createFrom == CreateFrom.EXISTING) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
) {
|
||||
for ((componentName, component) in components) {
|
||||
RadioListItem(
|
||||
onClick = { selectedComponentName = componentName },
|
||||
selected = selectedComponentName == componentName,
|
||||
text = component.label,
|
||||
secondaryText = componentName.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (createFrom == CreateFrom.EMPTY) {
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
text = stringRes(R.string.ext__editor__create_component__from_empty_warning),
|
||||
)
|
||||
DialogProperty(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__id),
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
value = newId,
|
||||
onValueChange = { newId = it },
|
||||
singleLine = true,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = newIdValidation,
|
||||
)
|
||||
}
|
||||
DialogProperty(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__label),
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
value = newLabel,
|
||||
onValueChange = { newLabel = it },
|
||||
singleLine = true,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = newLabelValidation,
|
||||
)
|
||||
}
|
||||
DialogProperty(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__authors),
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
value = newAuthors,
|
||||
onValueChange = { newAuthors = it },
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = newAuthorsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditorSheetTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
isRequired: Boolean = false,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
singleLine: Boolean = true,
|
||||
showValidationError: Boolean = false,
|
||||
validationResult: ValidationResult? = null,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colors.outline
|
||||
Column(modifier = Modifier.padding(vertical = TextFieldVerticalPadding)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TextFieldVerticalPadding),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
)
|
||||
if (isRequired) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 2.dp),
|
||||
text = "*",
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
FlorisOutlinedTextField(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
showValidationError = showValidationError,
|
||||
validationResult = validationResult,
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
unfocusedBorderColor = borderColor,
|
||||
disabledBorderColor = borderColor,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import dev.patrickgold.florisboard.res.ext.ExtensionDefaults
|
||||
fun ExtensionExportScreen(id: String) {
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
val ext = extensionManager.getExtensionById(id)
|
||||
if (ext != null) {
|
||||
ExportScreen(ext)
|
||||
|
||||
@@ -51,6 +51,7 @@ import dev.patrickgold.florisboard.app.ui.components.FlorisButtonBar
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.florisHorizontalScroll
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
@@ -213,7 +214,7 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__import__no_files_selected),
|
||||
text = stringRes(R.string.state__no_files_selected),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
@@ -251,9 +252,7 @@ private fun FileInfoView(
|
||||
fileInfo: CacheManager.FileInfo,
|
||||
) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = fileInfo.file.name,
|
||||
subtitle = fileInfo.mediaType ?: "application/unknown",
|
||||
) {
|
||||
|
||||
@@ -69,7 +69,7 @@ fun ExtensionMaintainerChip(
|
||||
if (maintainer.email != null) {
|
||||
FlorisChip(
|
||||
onClick = { context.launchUrl("mailto:${maintainer.email}") },
|
||||
text = maintainer.email,
|
||||
text = maintainer.email.toString(),
|
||||
leadingIcons = listOf(R.drawable.ic_email),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -28,11 +27,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -44,12 +40,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
@@ -58,23 +50,21 @@ import dev.patrickgold.florisboard.app.ui.components.FlorisConfirmDeleteDialog
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisHyperlinkText
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentImpl
|
||||
import dev.patrickgold.florisboard.res.FlorisRef
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMaintainer
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
|
||||
private val ComponentCardShape = RoundedCornerShape(8.dp)
|
||||
|
||||
@Composable
|
||||
fun ExtensionViewScreen(id: String) {
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
val ext = extensionManager.getExtensionById(id)
|
||||
if (ext != null) {
|
||||
ViewScreen(ext)
|
||||
@@ -169,13 +159,24 @@ private fun ViewScreen(ext: Extension) = FlorisScreen {
|
||||
text = stringRes(R.string.action__export),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
text = stringRes(R.string.ext__meta__components),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
ExtensionComponentListView(ext)
|
||||
}
|
||||
|
||||
when (ext) {
|
||||
is ThemeExtension -> {
|
||||
ExtensionComponentListView(
|
||||
title = stringRes(R.string.ext__meta__components_theme),
|
||||
components = ext.themes,
|
||||
) { component ->
|
||||
ExtensionComponentView(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
meta = ext.meta,
|
||||
component = component,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (extToDelete != null) {
|
||||
@@ -200,82 +201,6 @@ private fun ViewScreen(ext: Extension) = FlorisScreen {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionComponentListView(ext: Extension) {
|
||||
val components = ext.rememberComponents()
|
||||
if (components.isEmpty()) {
|
||||
ExtensionComponentNoneFoundView()
|
||||
} else {
|
||||
for (component in components) {
|
||||
ExtensionComponentView(ext, component) {
|
||||
when (component) {
|
||||
is ThemeExtensionComponent -> {
|
||||
val text = remember(component) {
|
||||
buildString {
|
||||
appendLine("authors = ${component.authors}")
|
||||
appendLine("isNightTheme = ${component.isNightTheme}")
|
||||
appendLine("isBorderless = ${component.isBorderless}")
|
||||
appendLine("isMaterialYouAware = ${component.isMaterialYouAware}")
|
||||
append("stylesheetPath = ${component.stylesheetPath()}")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ExtensionComponentNoneFoundView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionComponentView(
|
||||
ext: Extension,
|
||||
component: ExtensionComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val componentName = remember { ExtensionComponentName(ext.meta.id, component.id) }
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), ComponentCardShape)
|
||||
.padding(vertical = 8.dp, horizontal = 14.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(when (component) {
|
||||
is ThemeExtensionComponent -> R.string.ext__meta__components_label_theme
|
||||
else -> R.string.ext__meta__components_label_generic
|
||||
}, "label" to component.label),
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
text = remember { componentName.toString() },
|
||||
color = LocalContentColor.current.copy(alpha = 0.56f),
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionComponentNoneFoundView() {
|
||||
Text(
|
||||
text = stringRes(R.string.ext__meta__components_none_found),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionMetaRowSimpleText(
|
||||
label: String,
|
||||
@@ -343,7 +268,7 @@ private fun PreviewExtensionViewerScreen() {
|
||||
),
|
||||
dependencies = null,
|
||||
themes = listOf(
|
||||
ThemeExtensionComponent(id = "test", label = "Test", authors = listOf(), stylesheetPath = "test.json"),
|
||||
ThemeExtensionComponentImpl(id = "test", label = "Test", authors = listOf(), stylesheetPath = "test.json"),
|
||||
),
|
||||
)
|
||||
ViewScreen(ext = testExtension)
|
||||
|
||||
@@ -52,7 +52,7 @@ import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
@Composable
|
||||
fun HomeScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__home__title)
|
||||
backArrowVisible = false
|
||||
navigationIconVisible = false
|
||||
previewFieldVisible = true
|
||||
|
||||
val navController = LocalNavController.current
|
||||
@@ -109,13 +109,11 @@ fun HomeScreen() = FlorisScreen {
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Current version: ${BuildConfig.VERSION_NAME}\n")
|
||||
Text("List of unavailable features (and when they will get re-implemented):\n")
|
||||
Text(" - In-app theme customization (beta09) -> Theme import/export of custom themes is already possible, also the new engine is already pretty advanced and usable with imported theme extensions")
|
||||
Text(" - Glide typing bug fixes (beta09) -> glide works somewhat but long words tend to not get recognized")
|
||||
Text(" - Emoji view (beta10)")
|
||||
Text(" - Landscape fullscreen input (beta10)")
|
||||
Text(" - Word suggestions (will just show word + a number to test out if the UI works) (beta10+, new suggestion algorithm 0.3.15/16)\n")
|
||||
Text("Please do not file issues that these features do not work while the current version is below the intended re-implementation version. Thank you!\n")
|
||||
Text("List of unavailable features, will get implemented/fixed in the upcoming beta releases:\n")
|
||||
Text(" - Glide typing bug fixes -> glide works somewhat but long words tend to not get recognized")
|
||||
Text(" - Basic emoji view")
|
||||
Text(" - Word suggestions (will just show word + a number to test out if the UI works) (new suggestions in 0.4.0)\n")
|
||||
Text("Please do not file issues that above features do not work (especially word suggestions, it is more than known by now and the major goal for 0.4.0 after the preference rework and its hotfix phase has been completed). Thank you!\n")
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,18 +19,24 @@ package dev.patrickgold.florisboard.app.ui.settings.advanced
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.AppTheme
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.Routes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.common.FlorisLocale
|
||||
import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
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.listPrefEntries
|
||||
|
||||
@Composable
|
||||
fun AdvancedScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__advanced__title)
|
||||
previewFieldVisible = true
|
||||
previewFieldVisible = false
|
||||
|
||||
val navController = LocalNavController.current
|
||||
|
||||
content {
|
||||
ListPreference(
|
||||
@@ -126,5 +132,18 @@ fun AdvancedScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.pref__advanced__force_private_mode__label),
|
||||
summary = stringRes(R.string.pref__advanced__force_private_mode__summary),
|
||||
)
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.backup_and_restore__title)) {
|
||||
Preference(
|
||||
onClick = { navController.navigate(Routes.Settings.Backup) },
|
||||
title = stringRes(R.string.backup_and_restore__back_up__title),
|
||||
summary = stringRes(R.string.backup_and_restore__back_up__summary),
|
||||
)
|
||||
Preference(
|
||||
onClick = { navController.navigate(Routes.Settings.Restore) },
|
||||
title = stringRes(R.string.backup_and_restore__restore__title),
|
||||
summary = stringRes(R.string.backup_and_restore__restore__summary),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.advanced
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.RadioButton
|
||||
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.platform.LocalContext
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisButtonBar
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.rippleClickable
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.common.android.writeFromFile
|
||||
import dev.patrickgold.florisboard.res.FileRegistry
|
||||
import dev.patrickgold.florisboard.res.ZipUtils
|
||||
import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionManager
|
||||
import dev.patrickgold.florisboard.res.io.subDir
|
||||
import dev.patrickgold.florisboard.res.io.subFile
|
||||
import dev.patrickgold.florisboard.res.io.writeJson
|
||||
import dev.patrickgold.jetpref.datastore.jetprefDatastoreDir
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object Backup {
|
||||
const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider.file"
|
||||
const val METADATA_JSON_NAME = "backup_metadata.json"
|
||||
|
||||
fun defaultFileName(metadata: Metadata): String {
|
||||
return "backup_${metadata.packageName}_${metadata.versionCode}_${metadata.timestamp}.zip"
|
||||
}
|
||||
|
||||
enum class Destination {
|
||||
FILE_SYS,
|
||||
SHARE_INTENT;
|
||||
}
|
||||
|
||||
class FilesSelector {
|
||||
var jetprefDatastore by mutableStateOf(true)
|
||||
var imeKeyboard by mutableStateOf(true)
|
||||
var imeSpelling by mutableStateOf(true)
|
||||
var imeTheme by mutableStateOf(true)
|
||||
|
||||
fun atLeastOneSelected(): Boolean {
|
||||
return jetprefDatastore || imeKeyboard || imeSpelling || imeTheme
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
@SerialName("package")
|
||||
val packageName: String,
|
||||
val versionCode: Int,
|
||||
val versionName: String,
|
||||
val timestamp: Long,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackupScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.backup_and_restore__back_up__title)
|
||||
previewFieldVisible = false
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val cacheManager by context.cacheManager()
|
||||
|
||||
var backupDestination by remember { mutableStateOf(Backup.Destination.FILE_SYS) }
|
||||
val backupFilesSelector = remember { Backup.FilesSelector() }
|
||||
var backupWorkspace: CacheManager.BackupAndRestoreWorkspace? = null
|
||||
|
||||
val backUpToFileSystemLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument(),
|
||||
onResult = { uri ->
|
||||
if (uri == null) {
|
||||
// User can modify checkboxes between cancellation and second
|
||||
// trigger, so we make sure to clear out the previous workspace
|
||||
backupWorkspace?.close()
|
||||
backupWorkspace = null
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
runCatching {
|
||||
context.contentResolver.writeFromFile(uri, backupWorkspace!!.zipFile)
|
||||
backupWorkspace!!.close()
|
||||
}.onSuccess {
|
||||
context.showLongToast(R.string.backup_and_restore__back_up__success)
|
||||
navController.popBackStack()
|
||||
}.onFailure { error ->
|
||||
context.showLongToast(R.string.backup_and_restore__back_up__failure, "error_message" to error.localizedMessage)
|
||||
backupWorkspace = null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun prepareBackupWorkspace() {
|
||||
val workspace = cacheManager.backupAndRestore.new()
|
||||
if (backupFilesSelector.jetprefDatastore) {
|
||||
context.jetprefDatastoreDir.let { dir ->
|
||||
dir.copyRecursively(workspace.inputDir.subDir(dir.name))
|
||||
}
|
||||
}
|
||||
val workspaceFilesDir = workspace.inputDir.subDir("files")
|
||||
if (backupFilesSelector.imeKeyboard) {
|
||||
context.filesDir.subDir(ExtensionManager.IME_KEYBOARD_PATH).let { dir ->
|
||||
dir.copyRecursively(workspaceFilesDir.subDir(ExtensionManager.IME_KEYBOARD_PATH))
|
||||
}
|
||||
}
|
||||
if (backupFilesSelector.imeSpelling) {
|
||||
context.filesDir.subDir(ExtensionManager.IME_SPELLING_PATH).let { dir ->
|
||||
dir.copyRecursively(workspaceFilesDir.subDir(ExtensionManager.IME_SPELLING_PATH))
|
||||
}
|
||||
}
|
||||
if (backupFilesSelector.imeTheme) {
|
||||
context.filesDir.subDir(ExtensionManager.IME_THEME_PATH).let { dir ->
|
||||
dir.copyRecursively(workspaceFilesDir.subDir(ExtensionManager.IME_THEME_PATH))
|
||||
}
|
||||
}
|
||||
workspace.metadata = Backup.Metadata(
|
||||
packageName = BuildConfig.APPLICATION_ID,
|
||||
versionCode = BuildConfig.VERSION_CODE,
|
||||
versionName = BuildConfig.VERSION_NAME,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
)
|
||||
workspace.inputDir.subFile(Backup.METADATA_JSON_NAME).writeJson(workspace.metadata)
|
||||
workspace.zipFile = workspace.outputDir.subFile(Backup.defaultFileName(workspace.metadata))
|
||||
ZipUtils.zip(workspace.inputDir, workspace.zipFile)
|
||||
backupWorkspace = workspace
|
||||
}
|
||||
|
||||
fun prepareAndPerformBackup() {
|
||||
runCatching {
|
||||
if (backupWorkspace == null || backupWorkspace!!.isClosed()) {
|
||||
prepareBackupWorkspace()
|
||||
}
|
||||
when (backupDestination) {
|
||||
Backup.Destination.FILE_SYS -> {
|
||||
backUpToFileSystemLauncher.launch(backupWorkspace!!.zipFile.name)
|
||||
}
|
||||
Backup.Destination.SHARE_INTENT -> {
|
||||
val uri = FileProvider.getUriForFile(context, Backup.FILE_PROVIDER_AUTHORITY, backupWorkspace!!.zipFile)
|
||||
val shareIntent = ShareCompat.IntentBuilder(context)
|
||||
.setStream(uri)
|
||||
.setType(FileRegistry.BackupArchive.mediaType)
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
}
|
||||
}.onFailure { error ->
|
||||
context.showLongToast(R.string.backup_and_restore__back_up__failure, "error_message" to error.localizedMessage)
|
||||
backupWorkspace = null
|
||||
}
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(
|
||||
onClick = {
|
||||
backupWorkspace?.close()
|
||||
navController.popBackStack()
|
||||
},
|
||||
text = stringRes(R.string.action__cancel),
|
||||
)
|
||||
ButtonBarButton(
|
||||
onClick = {
|
||||
prepareAndPerformBackup()
|
||||
},
|
||||
text = stringRes(R.string.action__back_up),
|
||||
enabled = backupFilesSelector.atLeastOneSelected(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = stringRes(R.string.backup_and_restore__back_up__destination),
|
||||
) {
|
||||
RadioListItem(
|
||||
onClick = {
|
||||
backupDestination = Backup.Destination.FILE_SYS
|
||||
},
|
||||
selected = backupDestination == Backup.Destination.FILE_SYS,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__destination_file_sys),
|
||||
)
|
||||
RadioListItem(
|
||||
onClick = {
|
||||
backupDestination = Backup.Destination.SHARE_INTENT
|
||||
},
|
||||
selected = backupDestination == Backup.Destination.SHARE_INTENT,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__destination_share_intent),
|
||||
)
|
||||
}
|
||||
BackupFilesSelector(
|
||||
filesSelector = backupFilesSelector,
|
||||
title = stringRes(R.string.backup_and_restore__back_up__files),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BackupFilesSelector(
|
||||
modifier: Modifier = Modifier,
|
||||
filesSelector: Backup.FilesSelector,
|
||||
title: String,
|
||||
) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = modifier.defaultFlorisOutlinedBox(),
|
||||
title = title,
|
||||
) {
|
||||
CheckboxListItem(
|
||||
onClick = { filesSelector.jetprefDatastore = !filesSelector.jetprefDatastore },
|
||||
checked = filesSelector.jetprefDatastore,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__files_jetpref_datastore),
|
||||
)
|
||||
CheckboxListItem(
|
||||
onClick = { filesSelector.imeKeyboard = !filesSelector.imeKeyboard },
|
||||
checked = filesSelector.imeKeyboard,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__files_ime_keyboard),
|
||||
)
|
||||
CheckboxListItem(
|
||||
onClick = { filesSelector.imeSpelling = !filesSelector.imeSpelling },
|
||||
checked = filesSelector.imeSpelling,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__files_ime_spelling),
|
||||
)
|
||||
CheckboxListItem(
|
||||
onClick = { filesSelector.imeTheme = !filesSelector.imeTheme },
|
||||
checked = filesSelector.imeTheme,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__files_ime_theme),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CheckboxListItem(
|
||||
onClick: () -> Unit,
|
||||
checked: Boolean,
|
||||
text: String,
|
||||
) {
|
||||
JetPrefListItem(
|
||||
modifier = Modifier.rippleClickable(onClick = onClick),
|
||||
icon = {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
},
|
||||
text = text,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun RadioListItem(
|
||||
onClick: () -> Unit,
|
||||
selected: Boolean,
|
||||
text: String,
|
||||
secondaryText: String? = null,
|
||||
) {
|
||||
JetPrefListItem(
|
||||
modifier = Modifier.rippleClickable(onClick = onClick),
|
||||
icon = {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null,
|
||||
)
|
||||
},
|
||||
text = text,
|
||||
secondaryText = secondaryText,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.advanced
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.CardDefaults
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisButtonBar
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.common.android.readToFile
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.res.FileRegistry
|
||||
import dev.patrickgold.florisboard.res.ZipUtils
|
||||
import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionManager
|
||||
import dev.patrickgold.florisboard.res.io.deleteContentsRecursively
|
||||
import dev.patrickgold.florisboard.res.io.readJson
|
||||
import dev.patrickgold.florisboard.res.io.subDir
|
||||
import dev.patrickgold.florisboard.res.io.subFile
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
object Restore {
|
||||
const val MIN_VERSION_CODE = 64
|
||||
const val PACKAGE_NAME = "dev.patrickgold.florisboard"
|
||||
const val BACKUP_ARCHIVE_FILE_NAME = "backup.zip"
|
||||
|
||||
enum class Mode {
|
||||
MERGE,
|
||||
ERASE_AND_OVERWRITE;
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RestoreScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.backup_and_restore__restore__title)
|
||||
previewFieldVisible = false
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val cacheManager by context.cacheManager()
|
||||
|
||||
val restoreFilesSelector = remember { Backup.FilesSelector() }
|
||||
var restoreMode by remember { mutableStateOf(Restore.Mode.MERGE) }
|
||||
// TODO: rememberCoroutineScope() is unusable because it provides the scope in a cancelled state, which does
|
||||
// not make sense at all. I suspect that this is a bug and once it is resolved we can use it here again.
|
||||
val restoreScope = remember { CoroutineScope(Dispatchers.Main) }
|
||||
var restoreWorkspace by remember {
|
||||
mutableStateOf<CacheManager.BackupAndRestoreWorkspace?>(null)
|
||||
}
|
||||
|
||||
val restoreDataFromFileSystemLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
onResult = { uri ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
runCatching {
|
||||
restoreWorkspace?.close()
|
||||
restoreWorkspace = null
|
||||
val workspace = cacheManager.backupAndRestore.new()
|
||||
workspace.zipFile = workspace.inputDir.subFile(Restore.BACKUP_ARCHIVE_FILE_NAME)
|
||||
context.contentResolver.readToFile(uri, workspace.zipFile)
|
||||
ZipUtils.unzip(workspace.zipFile, workspace.outputDir)
|
||||
workspace.metadata = workspace.outputDir.subFile(Backup.METADATA_JSON_NAME).readJson()
|
||||
workspace.restoreWarningId = when {
|
||||
workspace.metadata.versionCode != BuildConfig.VERSION_CODE -> {
|
||||
R.string.backup_and_restore__restore__metadata_warn_different_version
|
||||
}
|
||||
!workspace.metadata.packageName.startsWith(Restore.PACKAGE_NAME) -> {
|
||||
R.string.backup_and_restore__restore__metadata_warn_different_vendor
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
workspace.restoreErrorId = when {
|
||||
workspace.metadata.packageName.isBlank() || workspace.metadata.versionCode < Restore.MIN_VERSION_CODE -> {
|
||||
R.string.backup_and_restore__restore__metadata_error_invalid_metadata
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
restoreWorkspace = workspace
|
||||
}.onFailure { error ->
|
||||
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to error.localizedMessage)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
suspend fun performRestore() {
|
||||
val prefs by florisPreferenceModel()
|
||||
val workspace = restoreWorkspace!!
|
||||
val shouldReset = restoreMode == Restore.Mode.ERASE_AND_OVERWRITE
|
||||
if (restoreFilesSelector.jetprefDatastore) {
|
||||
val datastoreFile = workspace.outputDir
|
||||
.subDir(JetPref.JETPREF_DIR_NAME)
|
||||
.subFile("${prefs.name}.${JetPref.JETPREF_FILE_EXT}")
|
||||
if (datastoreFile.exists()) {
|
||||
prefs.datastorePersistenceHandler?.loadPrefs(datastoreFile, shouldReset)
|
||||
prefs.datastorePersistenceHandler?.persistPrefs()
|
||||
}
|
||||
}
|
||||
val workspaceFilesDir = workspace.outputDir.subDir("files")
|
||||
if (restoreFilesSelector.imeKeyboard) {
|
||||
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_KEYBOARD_PATH)
|
||||
val dstDir = context.filesDir.subDir(ExtensionManager.IME_KEYBOARD_PATH)
|
||||
if (shouldReset) {
|
||||
dstDir.deleteContentsRecursively()
|
||||
}
|
||||
if (srcDir.exists()) {
|
||||
srcDir.copyRecursively(dstDir, overwrite = true)
|
||||
}
|
||||
}
|
||||
if (restoreFilesSelector.imeSpelling) {
|
||||
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_SPELLING_PATH)
|
||||
val dstDir = context.filesDir.subDir(ExtensionManager.IME_SPELLING_PATH)
|
||||
if (shouldReset) {
|
||||
dstDir.deleteContentsRecursively()
|
||||
}
|
||||
if (srcDir.exists()) {
|
||||
srcDir.copyRecursively(dstDir, overwrite = true)
|
||||
}
|
||||
}
|
||||
if (restoreFilesSelector.imeTheme) {
|
||||
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_THEME_PATH)
|
||||
val dstDir = context.filesDir.subDir(ExtensionManager.IME_THEME_PATH)
|
||||
if (shouldReset) {
|
||||
dstDir.deleteContentsRecursively()
|
||||
}
|
||||
if (srcDir.exists()) {
|
||||
srcDir.copyRecursively(dstDir, overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(
|
||||
onClick = {
|
||||
restoreWorkspace?.close()
|
||||
navController.popBackStack()
|
||||
},
|
||||
text = stringRes(R.string.action__cancel),
|
||||
)
|
||||
ButtonBarButton(
|
||||
onClick = {
|
||||
restoreScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
performRestore()
|
||||
context.showLongToast(R.string.backup_and_restore__restore__success)
|
||||
navController.popBackStack()
|
||||
} catch (e: Throwable) {
|
||||
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to e.localizedMessage)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = stringRes(R.string.action__restore),
|
||||
enabled = restoreWorkspace != null && restoreWorkspace?.restoreErrorId == null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = stringRes(R.string.backup_and_restore__restore__mode),
|
||||
) {
|
||||
RadioListItem(
|
||||
onClick = {
|
||||
restoreMode = Restore.Mode.MERGE
|
||||
},
|
||||
selected = restoreMode == Restore.Mode.MERGE,
|
||||
text = stringRes(R.string.backup_and_restore__restore__mode_merge),
|
||||
)
|
||||
RadioListItem(
|
||||
onClick = {
|
||||
restoreMode = Restore.Mode.ERASE_AND_OVERWRITE
|
||||
},
|
||||
selected = restoreMode == Restore.Mode.ERASE_AND_OVERWRITE,
|
||||
text = stringRes(R.string.backup_and_restore__restore__mode_erase_and_overwrite),
|
||||
)
|
||||
}
|
||||
FlorisOutlinedButton(
|
||||
onClick = {
|
||||
runCatching {
|
||||
restoreDataFromFileSystemLauncher.launch(
|
||||
FileRegistry.BackupArchive.mediaType
|
||||
)
|
||||
}.onFailure { error ->
|
||||
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to error.localizedMessage)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = stringRes(R.string.action__select_file),
|
||||
)
|
||||
val workspace = restoreWorkspace
|
||||
if (workspace == null) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.state__no_file_selected),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
} else {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = stringRes(R.string.backup_and_restore__restore__metadata),
|
||||
) {
|
||||
this@content.Preference(
|
||||
iconId = R.drawable.ic_code,
|
||||
title = workspace.metadata.packageName,
|
||||
)
|
||||
this@content.Preference(
|
||||
iconId = R.drawable.ic_info,
|
||||
title = "${workspace.metadata.versionName} (${workspace.metadata.versionCode})",
|
||||
)
|
||||
this@content.Preference(
|
||||
iconId = R.drawable.ic_schedule,
|
||||
title = remember(workspace.metadata.timestamp) {
|
||||
val formatter = DateFormat.getDateTimeInstance()
|
||||
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
calendar.timeInMillis = workspace.metadata.timestamp
|
||||
formatter.format(calendar.time)
|
||||
},
|
||||
)
|
||||
if (workspace.restoreErrorId != null) {
|
||||
Column(modifier = Modifier.padding(CardDefaults.ContentPadding)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(9.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
.background(MaterialTheme.colors.error.copy(alpha = 0.56f))
|
||||
)
|
||||
Text(
|
||||
text = stringRes(workspace.restoreErrorId!!),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.error,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
} else if (workspace.restoreWarningId != null) {
|
||||
Column(modifier = Modifier.padding(CardDefaults.ContentPadding)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(9.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
.background(LocalContentColor.current.copy(alpha = LocalContentAlpha.current))
|
||||
)
|
||||
Text(
|
||||
text = stringRes(workspace.restoreErrorId!!),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (workspace.restoreErrorId == null) {
|
||||
BackupFilesSelector(
|
||||
filesSelector = restoreFilesSelector,
|
||||
title = stringRes(R.string.backup_and_restore__restore__files),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,11 @@ fun KeyboardScreen() = FlorisScreen {
|
||||
entries = UtilityKeyAction.listEntries(),
|
||||
visibleIf = { prefs.keyboard.utilityKeyEnabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.keyboard.spaceBarLanguageDisplayEnabled,
|
||||
title = stringRes(R.string.pref__keyboard__space_bar_language_display_enabled__label),
|
||||
summary = stringRes(R.string.pref__keyboard__space_bar_language_display_enabled__summary),
|
||||
)
|
||||
DialogSliderPreference(
|
||||
primaryPref = prefs.keyboard.fontSizeMultiplierPortrait,
|
||||
secondaryPref = prefs.keyboard.fontSizeMultiplierLandscape,
|
||||
|
||||
@@ -193,7 +193,7 @@ fun ImportSpellingArchiveScreen() = FlorisScreen {
|
||||
StepButton(
|
||||
onClick = {
|
||||
runCatching {
|
||||
extensionManager.import(importArchiveEditor!!.build().getOrThrow())
|
||||
extensionManager.import(importArchiveEditor!!.build())
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
navController.popBackStack()
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.common.kotlin.curlyFormat
|
||||
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
|
||||
|
||||
/**
|
||||
* DisplayColorsAs indicates how color strings should be visually presented to the user.
|
||||
*/
|
||||
enum class DisplayColorsAs {
|
||||
HEX8,
|
||||
RGBA;
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun listEntries() = listPrefEntries {
|
||||
entry(
|
||||
key = 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 = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isSpecified
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.DpSizeSaver
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisChip
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisDropdownMenu
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedTextField
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisTextButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.rippleClickable
|
||||
import dev.patrickgold.florisboard.common.ValidationResult
|
||||
import dev.patrickgold.florisboard.common.kotlin.curlyFormat
|
||||
import dev.patrickgold.florisboard.common.rememberValidationResult
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionValidation
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.snygg.SnyggPropertySetSpec
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDefinedVarValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggImplicitInheritValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValueEncoder
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggVarValueEncoders
|
||||
import dev.patrickgold.jetpref.material.ui.ExperimentalJetPrefMaterialUi
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefColorPicker
|
||||
import dev.patrickgold.jetpref.material.ui.rememberJetPrefColorPickerState
|
||||
|
||||
internal val SnyggEmptyPropertyInfoForAdding = PropertyInfo(
|
||||
name = "- select -",
|
||||
value = SnyggImplicitInheritValue,
|
||||
)
|
||||
|
||||
data class PropertyInfo(
|
||||
val name: String,
|
||||
val value: SnyggValue,
|
||||
)
|
||||
|
||||
private enum class ShapeCorner {
|
||||
TOP_START,
|
||||
TOP_END,
|
||||
BOTTOM_END,
|
||||
BOTTOM_START;
|
||||
|
||||
@Composable
|
||||
fun label(): String {
|
||||
return stringRes(when (this) {
|
||||
TOP_START -> R.string.enum__shape_corner__top_start
|
||||
TOP_END -> R.string.enum__shape_corner__top_end
|
||||
BOTTOM_END -> R.string.enum__shape_corner__bottom_end
|
||||
BOTTOM_START -> R.string.enum__shape_corner__bottom_start
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun EditPropertyDialog(
|
||||
propertySetSpec: SnyggPropertySetSpec?,
|
||||
initProperty: PropertyInfo,
|
||||
level: SnyggLevel,
|
||||
displayColorsAs: DisplayColorsAs,
|
||||
definedVariables: Map<String, SnyggValue>,
|
||||
onConfirmNewValue: (String, SnyggValue) -> Boolean,
|
||||
onDelete: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val isAddPropertyDialog = initProperty == SnyggEmptyPropertyInfoForAdding
|
||||
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
|
||||
var showAlreadyExistsError by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var propertyName by rememberSaveable {
|
||||
mutableStateOf(if (isAddPropertyDialog && propertySetSpec == null) { "" } else { initProperty.name })
|
||||
}
|
||||
val propertyNameValidation = rememberValidationResult(ExtensionValidation.ThemeComponentVariableName, propertyName)
|
||||
var propertyValueEncoder by remember {
|
||||
mutableStateOf(if (isAddPropertyDialog && propertySetSpec == null) {
|
||||
SnyggImplicitInheritValue
|
||||
} else {
|
||||
initProperty.value.encoder()
|
||||
})
|
||||
}
|
||||
var propertyValue by remember {
|
||||
mutableStateOf(if (isAddPropertyDialog && propertySetSpec == null) {
|
||||
SnyggImplicitInheritValue
|
||||
} else {
|
||||
initProperty.value
|
||||
})
|
||||
}
|
||||
|
||||
fun isPropertyNameValid(): Boolean {
|
||||
return propertyName.isNotBlank() && propertyName != SnyggEmptyPropertyInfoForAdding.name
|
||||
}
|
||||
|
||||
fun isPropertyValueValid(): Boolean {
|
||||
return when (val value = propertyValue) {
|
||||
is SnyggImplicitInheritValue -> false
|
||||
is SnyggDefinedVarValue -> value.key.isNotBlank()
|
||||
is SnyggSpSizeValue -> value.sp.isSpecified && value.sp.value >= 1f
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(if (isAddPropertyDialog) {
|
||||
R.string.settings__theme_editor__add_property
|
||||
} else {
|
||||
R.string.settings__theme_editor__edit_property
|
||||
}),
|
||||
confirmLabel = stringRes(if (isAddPropertyDialog) {
|
||||
R.string.action__add
|
||||
} else {
|
||||
R.string.action__apply
|
||||
}),
|
||||
onConfirm = {
|
||||
if (!isPropertyNameValid() || !isPropertyValueValid()) {
|
||||
showSelectAsError = true
|
||||
} else {
|
||||
if (!onConfirmNewValue(propertyName, propertyValue)) {
|
||||
showAlreadyExistsError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = onDismiss,
|
||||
neutralLabel = if (!isAddPropertyDialog) { stringRes(R.string.action__delete) } else { null },
|
||||
onNeutral = onDelete,
|
||||
neutralColors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colors.error,
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(visible = showAlreadyExistsError) {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = stringRes(R.string.settings__theme_editor__property_already_exists),
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
}
|
||||
|
||||
DialogProperty(text = stringRes(R.string.settings__theme_editor__property_name)) {
|
||||
PropertyNameInput(
|
||||
propertySetSpec = propertySetSpec,
|
||||
name = propertyName,
|
||||
nameValidation = propertyNameValidation,
|
||||
onNameChange = { name ->
|
||||
if (propertySetSpec != null) {
|
||||
propertyValueEncoder = SnyggImplicitInheritValue
|
||||
}
|
||||
propertyName = name
|
||||
},
|
||||
level = level,
|
||||
isAddPropertyDialog = isAddPropertyDialog,
|
||||
showSelectAsError = showSelectAsError,
|
||||
)
|
||||
}
|
||||
|
||||
DialogProperty(text = stringRes(R.string.settings__theme_editor__property_value)) {
|
||||
PropertyValueEncoderDropdown(
|
||||
supportedEncoders = remember(propertyName) {
|
||||
propertySetSpec?.propertySpec(propertyName)?.encoders ?: SnyggVarValueEncoders
|
||||
},
|
||||
encoder = propertyValueEncoder,
|
||||
onEncoderChange = { encoder ->
|
||||
propertyValueEncoder = encoder
|
||||
propertyValue = encoder.defaultValue()
|
||||
},
|
||||
enabled = isPropertyNameValid(),
|
||||
isError = showSelectAsError && propertyValueEncoder == SnyggImplicitInheritValue,
|
||||
)
|
||||
|
||||
PropertyValueEditor(
|
||||
value = propertyValue,
|
||||
onValueChange = { propertyValue = it },
|
||||
level = level,
|
||||
displayColorsAs = displayColorsAs,
|
||||
definedVariables = definedVariables,
|
||||
isError = showSelectAsError && !isPropertyValueValid(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PropertyNameInput(
|
||||
propertySetSpec: SnyggPropertySetSpec?,
|
||||
name: String,
|
||||
nameValidation: ValidationResult,
|
||||
onNameChange: (String) -> Unit,
|
||||
level: SnyggLevel,
|
||||
isAddPropertyDialog: Boolean,
|
||||
showSelectAsError: Boolean,
|
||||
) {
|
||||
if (propertySetSpec != null) {
|
||||
val possiblePropertyNames = remember(propertySetSpec) {
|
||||
listOf(SnyggEmptyPropertyInfoForAdding.name) + propertySetSpec.supportedProperties.map { it.name }
|
||||
}
|
||||
val possiblePropertyLabels = possiblePropertyNames.map { translatePropertyName(it, level) }
|
||||
var propertiesExpanded by remember { mutableStateOf(false) }
|
||||
val propertiesSelectedIndex = remember(name) {
|
||||
possiblePropertyNames.indexOf(name).coerceIn(possiblePropertyNames.indices)
|
||||
}
|
||||
FlorisDropdownMenu(
|
||||
items = possiblePropertyLabels,
|
||||
expanded = propertiesExpanded,
|
||||
enabled = isAddPropertyDialog,
|
||||
selectedIndex = propertiesSelectedIndex,
|
||||
isError = showSelectAsError && propertiesSelectedIndex == 0,
|
||||
onSelectItem = { index ->
|
||||
onNameChange(possiblePropertyNames[index])
|
||||
},
|
||||
onExpandRequest = { propertiesExpanded = true },
|
||||
onDismissRequest = { propertiesExpanded = false },
|
||||
)
|
||||
} else {
|
||||
FlorisOutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = onNameChange,
|
||||
enabled = isAddPropertyDialog,
|
||||
showValidationHint = isAddPropertyDialog,
|
||||
showValidationError = showSelectAsError,
|
||||
validationResult = nameValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PropertyValueEncoderDropdown(
|
||||
supportedEncoders: List<SnyggValueEncoder>,
|
||||
encoder: SnyggValueEncoder,
|
||||
onEncoderChange: (SnyggValueEncoder) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
val encoders = remember(supportedEncoders) {
|
||||
listOf(SnyggImplicitInheritValue) + supportedEncoders
|
||||
}
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val selectedIndex = remember(encoder) {
|
||||
encoders.indexOf(encoder).coerceIn(encoders.indices)
|
||||
}
|
||||
FlorisDropdownMenu(
|
||||
items = encoders,
|
||||
labelProvider = { translatePropertyValueEncoderName(it) },
|
||||
expanded = expanded,
|
||||
enabled = enabled,
|
||||
selectedIndex = selectedIndex,
|
||||
isError = isError,
|
||||
onSelectItem = { index ->
|
||||
onEncoderChange(encoders[index])
|
||||
},
|
||||
onExpandRequest = { expanded = true },
|
||||
onDismissRequest = { expanded = false },
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalJetPrefMaterialUi::class)
|
||||
@Composable
|
||||
private fun PropertyValueEditor(
|
||||
value: SnyggValue,
|
||||
onValueChange: (SnyggValue) -> Unit,
|
||||
level: SnyggLevel,
|
||||
displayColorsAs: DisplayColorsAs,
|
||||
definedVariables: Map<String, SnyggValue>,
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
when (value) {
|
||||
is SnyggDefinedVarValue -> {
|
||||
val variableKeys = remember(definedVariables) {
|
||||
listOf("") + definedVariables.keys.toList()
|
||||
}
|
||||
val selectedIndex by remember(variableKeys, value.key) {
|
||||
mutableStateOf(variableKeys.indexOf(value.key).coerceIn(variableKeys.indices))
|
||||
}
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
FlorisDropdownMenu(
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.weight(1f),
|
||||
items = variableKeys,
|
||||
labelProvider = { translatePropertyName(it, level) },
|
||||
expanded = expanded,
|
||||
selectedIndex = selectedIndex,
|
||||
isError = isError,
|
||||
onSelectItem = { index ->
|
||||
onValueChange(SnyggDefinedVarValue(variableKeys[index]))
|
||||
},
|
||||
onExpandRequest = { expanded = true },
|
||||
onDismissRequest = { expanded = false },
|
||||
)
|
||||
SnyggValueIcon(
|
||||
value = value,
|
||||
definedVariables = definedVariables,
|
||||
)
|
||||
}
|
||||
}
|
||||
is SnyggSolidColorValue -> {
|
||||
val colorPickerState = rememberJetPrefColorPickerState(initColor = value.color)
|
||||
val colorPickerStr = translatePropertyValue(value, level, displayColorsAs)
|
||||
var showEditColorStrDialog by rememberSaveable { mutableStateOf(false) }
|
||||
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.rippleClickable {
|
||||
showEditColorStrDialog = true
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.weight(1f),
|
||||
text = colorPickerStr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
SnyggValueIcon(
|
||||
value = value,
|
||||
definedVariables = definedVariables,
|
||||
)
|
||||
}
|
||||
JetPrefColorPicker(
|
||||
onColorChange = { onValueChange(SnyggSolidColorValue(it)) },
|
||||
state = colorPickerState,
|
||||
)
|
||||
}
|
||||
if (showEditColorStrDialog) {
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
var showSyntaxHelp by rememberSaveable { mutableStateOf(false) }
|
||||
var colorStr by rememberSaveable { mutableStateOf(colorPickerStr) }
|
||||
val colorStrValidation = rememberValidationResult(ExtensionValidation.SnyggSolidColorValue, colorStr)
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(R.string.settings__theme_editor__property_value_color_dialog_title),
|
||||
confirmLabel = stringRes(R.string.action__apply),
|
||||
onConfirm = {
|
||||
if (colorStrValidation.isInvalid()) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
val newValue = SnyggSolidColorValue.deserialize(colorStr.trim()).getOrThrow()
|
||||
onValueChange(newValue)
|
||||
colorPickerState.setColor((newValue as SnyggSolidColorValue).color)
|
||||
showEditColorStrDialog = false
|
||||
}
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = {
|
||||
showEditColorStrDialog = false
|
||||
},
|
||||
trailingIconTitle = {
|
||||
FlorisIconButton(
|
||||
onClick = { showSyntaxHelp = !showSyntaxHelp },
|
||||
modifier = Modifier.offset(x = 12.dp),
|
||||
icon = painterResource(R.drawable.ic_help_outline),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(visible = showSyntaxHelp) {
|
||||
Column(modifier = Modifier.padding(bottom = 16.dp)) {
|
||||
Text(text = "Supported color string syntaxes:")
|
||||
Text(
|
||||
text = """
|
||||
#RRGGBBAA
|
||||
-> all in 00h..FFh
|
||||
#RRGGBB
|
||||
-> all in 00h..FFh
|
||||
rgba(r,g,b,a)
|
||||
-> r,g,b in 0..255
|
||||
-> a in 0.0..1.0
|
||||
rgb(r,g,b)
|
||||
-> r,g,b in 0..255
|
||||
""".trimIndent(),
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
FlorisOutlinedTextField(
|
||||
value = colorStr,
|
||||
onValueChange = { colorStr = it },
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = colorStrValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is SnyggDpSizeValue -> {
|
||||
var sizeStr by remember {
|
||||
val dp = value.dp.takeUnless { it.isUnspecified } ?: SnyggDpSizeValue.defaultValue().dp
|
||||
mutableStateOf(dp.value.toString())
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = sizeStr,
|
||||
onValueChange = { value ->
|
||||
sizeStr = value
|
||||
val size = sizeStr.toFloatOrNull()?.let { SnyggDpSizeValue(it.dp) }
|
||||
onValueChange(size ?: SnyggDpSizeValue(Dp.Unspecified))
|
||||
},
|
||||
isError = value.dp.isUnspecified || value.dp.value < 1f,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = "dp",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
is SnyggSpSizeValue -> {
|
||||
var sizeStr by remember {
|
||||
val sp = value.sp.takeUnless { it.isUnspecified } ?: SnyggSpSizeValue.defaultValue().sp
|
||||
mutableStateOf(sp.value.toString())
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = sizeStr,
|
||||
onValueChange = { value ->
|
||||
sizeStr = value
|
||||
val size = sizeStr.toFloatOrNull()?.let { SnyggSpSizeValue(it.sp) }
|
||||
onValueChange(size ?: SnyggSpSizeValue(TextUnit.Unspecified))
|
||||
},
|
||||
isError = value.sp.isUnspecified || value.sp.value < 1f,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = "sp",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
is SnyggShapeValue -> when (value) {
|
||||
is SnyggDpShapeValue -> {
|
||||
var showDialogInitDp by rememberSaveable(stateSaver = DpSizeSaver) {
|
||||
mutableStateOf(0.dp)
|
||||
}
|
||||
var showDialogForCorner by rememberSaveable {
|
||||
mutableStateOf<ShapeCorner?>(null)
|
||||
}
|
||||
var topStart by rememberSaveable(stateSaver = DpSizeSaver) {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerDpShapeValue -> value.topStart
|
||||
is SnyggRoundedCornerDpShapeValue -> value.topStart
|
||||
})
|
||||
}
|
||||
var topEnd by rememberSaveable(stateSaver = DpSizeSaver) {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerDpShapeValue -> value.topEnd
|
||||
is SnyggRoundedCornerDpShapeValue -> value.topEnd
|
||||
})
|
||||
}
|
||||
var bottomEnd by rememberSaveable(stateSaver = DpSizeSaver) {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerDpShapeValue -> value.bottomEnd
|
||||
is SnyggRoundedCornerDpShapeValue -> value.bottomEnd
|
||||
})
|
||||
}
|
||||
var bottomStart by rememberSaveable(stateSaver = DpSizeSaver) {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerDpShapeValue -> value.bottomStart
|
||||
is SnyggRoundedCornerDpShapeValue -> value.bottomStart
|
||||
})
|
||||
}
|
||||
val shape = remember(topStart, topEnd, bottomEnd, bottomStart) {
|
||||
when (value) {
|
||||
is SnyggCutCornerDpShapeValue -> {
|
||||
CutCornerShape(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
is SnyggRoundedCornerDpShapeValue -> {
|
||||
RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(shape) {
|
||||
onValueChange(when (value) {
|
||||
is SnyggCutCornerDpShapeValue -> {
|
||||
SnyggCutCornerDpShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
is SnyggRoundedCornerDpShapeValue -> {
|
||||
SnyggRoundedCornerDpShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
})
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
Column {
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitDp = topStart
|
||||
showDialogForCorner = ShapeCorner.TOP_START
|
||||
},
|
||||
text = stringRes(R.string.unit__display_pixel__symbol).curlyFormat("v" to topStart.value),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitDp = bottomStart
|
||||
showDialogForCorner = ShapeCorner.BOTTOM_START
|
||||
},
|
||||
text = stringRes(R.string.unit__display_pixel__symbol).curlyFormat("v" to bottomStart.value),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredSize(40.dp)
|
||||
.border(1.dp, MaterialTheme.colors.onBackground, shape),
|
||||
)
|
||||
Column {
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitDp = topEnd
|
||||
showDialogForCorner = ShapeCorner.TOP_END
|
||||
},
|
||||
text = stringRes(R.string.unit__display_pixel__symbol).curlyFormat("v" to topEnd.value),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitDp = bottomEnd
|
||||
showDialogForCorner = ShapeCorner.BOTTOM_END
|
||||
},
|
||||
text = stringRes(R.string.unit__display_pixel__symbol).curlyFormat("v" to bottomEnd.value),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
val dialogForCorner = showDialogForCorner
|
||||
if (dialogForCorner != null) {
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
var size by rememberSaveable {
|
||||
mutableStateOf(showDialogInitDp.value.toString().removeSuffix(".0"))
|
||||
}
|
||||
val sizeValidation = rememberValidationResult(ExtensionValidation.SnyggDpShapeValue, size)
|
||||
JetPrefAlertDialog(
|
||||
title = dialogForCorner.label(),
|
||||
confirmLabel = stringRes(R.string.action__apply),
|
||||
onConfirm = {
|
||||
if (sizeValidation.isInvalid()) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
val sizeDp = size.toFloat().dp
|
||||
when (dialogForCorner) {
|
||||
ShapeCorner.TOP_START -> topStart = sizeDp
|
||||
ShapeCorner.TOP_END -> topEnd = sizeDp
|
||||
ShapeCorner.BOTTOM_END -> bottomEnd = sizeDp
|
||||
ShapeCorner.BOTTOM_START -> bottomStart = sizeDp
|
||||
}
|
||||
showDialogForCorner = null
|
||||
}
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = {
|
||||
showDialogForCorner = null
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
FlorisOutlinedTextField(
|
||||
value = size,
|
||||
onValueChange = { size = it },
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = sizeValidation,
|
||||
)
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
if (sizeValidation.isInvalid()) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
val sizeDp = size.toFloat().dp
|
||||
topStart = sizeDp
|
||||
topEnd = sizeDp
|
||||
bottomEnd = sizeDp
|
||||
bottomStart = sizeDp
|
||||
showDialogForCorner = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
text = stringRes(R.string.settings__theme_editor__property_value_shape_apply_for_all_corners),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is SnyggPercentShapeValue -> {
|
||||
var showDialogInitPercentage by rememberSaveable {
|
||||
mutableStateOf(0)
|
||||
}
|
||||
var showDialogForCorner by rememberSaveable {
|
||||
mutableStateOf<ShapeCorner?>(null)
|
||||
}
|
||||
var topStart by rememberSaveable {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerPercentShapeValue -> value.topStart
|
||||
is SnyggRoundedCornerPercentShapeValue -> value.topStart
|
||||
})
|
||||
}
|
||||
var topEnd by rememberSaveable {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerPercentShapeValue -> value.topEnd
|
||||
is SnyggRoundedCornerPercentShapeValue -> value.topEnd
|
||||
})
|
||||
}
|
||||
var bottomEnd by rememberSaveable {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerPercentShapeValue -> value.bottomEnd
|
||||
is SnyggRoundedCornerPercentShapeValue -> value.bottomEnd
|
||||
})
|
||||
}
|
||||
var bottomStart by rememberSaveable {
|
||||
mutableStateOf(when (value) {
|
||||
is SnyggCutCornerPercentShapeValue -> value.bottomStart
|
||||
is SnyggRoundedCornerPercentShapeValue -> value.bottomStart
|
||||
})
|
||||
}
|
||||
val shape = remember(topStart, topEnd, bottomEnd, bottomStart) {
|
||||
when (value) {
|
||||
is SnyggCutCornerPercentShapeValue -> {
|
||||
CutCornerShape(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
is SnyggRoundedCornerPercentShapeValue -> {
|
||||
RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(shape) {
|
||||
onValueChange(when (value) {
|
||||
is SnyggCutCornerPercentShapeValue -> {
|
||||
SnyggCutCornerPercentShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
is SnyggRoundedCornerPercentShapeValue -> {
|
||||
SnyggRoundedCornerPercentShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
})
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
Column {
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitPercentage = topStart
|
||||
showDialogForCorner = ShapeCorner.TOP_START
|
||||
},
|
||||
text = stringRes(R.string.unit__percent__symbol).curlyFormat("v" to topStart),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitPercentage = bottomStart
|
||||
showDialogForCorner = ShapeCorner.BOTTOM_START
|
||||
},
|
||||
text = stringRes(R.string.unit__percent__symbol).curlyFormat("v" to bottomStart),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredSize(40.dp)
|
||||
.border(1.dp, MaterialTheme.colors.onBackground, shape),
|
||||
)
|
||||
Column {
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitPercentage = topEnd
|
||||
showDialogForCorner = ShapeCorner.TOP_END
|
||||
},
|
||||
text = stringRes(R.string.unit__percent__symbol).curlyFormat("v" to topEnd),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
FlorisChip(
|
||||
onClick = {
|
||||
showDialogInitPercentage = bottomEnd
|
||||
showDialogForCorner = ShapeCorner.BOTTOM_END
|
||||
},
|
||||
text = stringRes(R.string.unit__percent__symbol).curlyFormat("v" to bottomEnd),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
val dialogForCorner = showDialogForCorner
|
||||
if (dialogForCorner != null) {
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
var size by rememberSaveable {
|
||||
mutableStateOf(showDialogInitPercentage.toString())
|
||||
}
|
||||
val sizeValidation = rememberValidationResult(ExtensionValidation.SnyggPercentShapeValue, size)
|
||||
JetPrefAlertDialog(
|
||||
title = dialogForCorner.label(),
|
||||
confirmLabel = stringRes(R.string.action__apply),
|
||||
onConfirm = {
|
||||
if (sizeValidation.isInvalid()) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
val sizePercentage = size.toInt()
|
||||
when (showDialogForCorner) {
|
||||
ShapeCorner.TOP_START -> topStart = sizePercentage
|
||||
ShapeCorner.TOP_END -> topEnd = sizePercentage
|
||||
ShapeCorner.BOTTOM_END -> bottomEnd = sizePercentage
|
||||
ShapeCorner.BOTTOM_START -> bottomStart = sizePercentage
|
||||
else -> { }
|
||||
}
|
||||
showDialogForCorner = null
|
||||
}
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = {
|
||||
showDialogForCorner = null
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
FlorisOutlinedTextField(
|
||||
value = size,
|
||||
onValueChange = { size = it },
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = sizeValidation,
|
||||
)
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
if (sizeValidation.isInvalid()) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
val sizePercentage = size.toInt()
|
||||
topStart = sizePercentage
|
||||
topEnd = sizePercentage
|
||||
bottomEnd = sizePercentage
|
||||
bottomStart = sizePercentage
|
||||
showDialogForCorner = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
text = stringRes(R.string.settings__theme_editor__property_value_shape_apply_for_all_corners),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredSize(40.dp)
|
||||
.border(1.dp, MaterialTheme.colors.onBackground, value.shape),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisChip
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisDropdownMenu
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisHyperlinkText
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedTextField
|
||||
import dev.patrickgold.florisboard.app.ui.components.florisHorizontalScroll
|
||||
import dev.patrickgold.florisboard.common.kotlin.curlyFormat
|
||||
import dev.patrickgold.florisboard.ime.nlp.NATIVE_NULLPTR
|
||||
import dev.patrickgold.florisboard.ime.text.key.InputMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUiSpec
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.snygg.SnyggRule
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
|
||||
internal val SnyggEmptyRuleForAdding = SnyggRule(element = "- select -")
|
||||
|
||||
@Composable
|
||||
internal fun EditRuleDialog(
|
||||
initRule: SnyggRule,
|
||||
level: SnyggLevel,
|
||||
onConfirmRule: (oldRule: SnyggRule, newRule: SnyggRule) -> Boolean,
|
||||
onDeleteRule: (rule: SnyggRule) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val isAddRuleDialog = initRule == SnyggEmptyRuleForAdding
|
||||
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
|
||||
var showAlreadyExistsError by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
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 = possibleElementNames.indexOf(initRule.element).coerceIn(possibleElementNames.indices)
|
||||
mutableStateOf(index)
|
||||
}
|
||||
|
||||
val codes = rememberSaveable(saver = IntListSaver) { initRule.codes.toMutableStateList() }
|
||||
var editCodeDialogValue by rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
val groups = rememberSaveable(saver = IntListSaver) { initRule.groups.toMutableStateList() }
|
||||
var modeNormal by rememberSaveable { mutableStateOf(initRule.modes.contains(InputMode.NORMAL.value)) }
|
||||
var modeShiftLock by rememberSaveable { mutableStateOf(initRule.modes.contains(InputMode.SHIFT_LOCK.value)) }
|
||||
var modeCapsLock by rememberSaveable { mutableStateOf(initRule.modes.contains(InputMode.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
|
||||
}),
|
||||
onConfirm = {
|
||||
if (isAddRuleDialog && elementsSelectedIndex == 0) {
|
||||
showSelectAsError = true
|
||||
} else {
|
||||
val newRule = SnyggRule(
|
||||
element = possibleElementNames[elementsSelectedIndex],
|
||||
codes = codes.toList(),
|
||||
groups = groups.toList(),
|
||||
modes = buildList {
|
||||
if (modeNormal) { add(InputMode.NORMAL.value) }
|
||||
if (modeShiftLock) { add(InputMode.SHIFT_LOCK.value) }
|
||||
if (modeCapsLock) { add(InputMode.CAPS_LOCK.value) }
|
||||
},
|
||||
pressedSelector = pressedSelector,
|
||||
focusSelector = focusSelector,
|
||||
disabledSelector = disabledSelector,
|
||||
)
|
||||
if (!onConfirmRule(initRule, newRule)) {
|
||||
showAlreadyExistsError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = onDismiss,
|
||||
neutralLabel = if (!isAddRuleDialog) {
|
||||
stringRes(R.string.action__delete)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
neutralColors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.error),
|
||||
onNeutral = { onDeleteRule(initRule) },
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(visible = showAlreadyExistsError) {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = stringRes(R.string.settings__theme_editor__rule_already_exists),
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
}
|
||||
|
||||
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 },
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
color = if (pressedSelector) MaterialTheme.colors.primaryVariant else Color.Unspecified,
|
||||
)
|
||||
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)
|
||||
},
|
||||
color = if (focusSelector) MaterialTheme.colors.primaryVariant else Color.Unspecified,
|
||||
)
|
||||
FlorisChip(
|
||||
onClick = { disabledSelector = !disabledSelector },
|
||||
text = when (level) {
|
||||
SnyggLevel.DEVELOPER -> SnyggRule.DISABLED_SELECTOR
|
||||
else -> stringRes(R.string.snygg__rule_selector__disabled)
|
||||
},
|
||||
color = if (disabledSelector) MaterialTheme.colors.primaryVariant else Color.Unspecified,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DialogProperty(
|
||||
text = stringRes(R.string.settings__theme_editor__rule_codes),
|
||||
trailingIconTitle = {
|
||||
FlorisIconButton(
|
||||
onClick = { editCodeDialogValue = NATIVE_NULLPTR },
|
||||
modifier = Modifier.offset(x = 12.dp),
|
||||
icon = painterResource(R.drawable.ic_add),
|
||||
)
|
||||
},
|
||||
) {
|
||||
if (codes.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
text = stringRes(R.string.settings__theme_editor__no_codes_defined),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
FlowRow {
|
||||
for (code in codes) {
|
||||
FlorisChip(
|
||||
onClick = { editCodeDialogValue = code },
|
||||
text = code.toString(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_modes)) {
|
||||
Row(modifier = Modifier.florisHorizontalScroll()) {
|
||||
FlorisChip(
|
||||
onClick = { modeNormal = !modeNormal },
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
text = when (level) {
|
||||
SnyggLevel.DEVELOPER -> remember { "m:${InputMode.NORMAL.toString().lowercase()}" }
|
||||
else -> stringRes(R.string.enum__input_mode__normal)
|
||||
},
|
||||
color = if (modeNormal) MaterialTheme.colors.primaryVariant else Color.Unspecified,
|
||||
)
|
||||
FlorisChip(
|
||||
onClick = { modeShiftLock = !modeShiftLock },
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
text = when (level) {
|
||||
SnyggLevel.DEVELOPER -> remember { "m:${InputMode.SHIFT_LOCK.toString().lowercase()}" }
|
||||
else -> stringRes(R.string.enum__input_mode__shift_lock)
|
||||
},
|
||||
color = if (modeShiftLock) MaterialTheme.colors.primaryVariant else Color.Unspecified,
|
||||
)
|
||||
FlorisChip(
|
||||
onClick = { modeCapsLock = !modeCapsLock },
|
||||
text = when (level) {
|
||||
SnyggLevel.DEVELOPER -> remember { "m:${InputMode.CAPS_LOCK.toString().lowercase()}" }
|
||||
else -> stringRes(R.string.enum__input_mode__caps_lock)
|
||||
},
|
||||
color = if (modeCapsLock) MaterialTheme.colors.primaryVariant else Color.Unspecified,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val initCodeValue = editCodeDialogValue
|
||||
if (initCodeValue != null) {
|
||||
var inputCodeString by rememberSaveable(initCodeValue) { mutableStateOf(initCodeValue.toString()) }
|
||||
var showKeyCodesHelp by rememberSaveable(initCodeValue) { mutableStateOf(false) }
|
||||
var showError by rememberSaveable(initCodeValue) { mutableStateOf(false) }
|
||||
var errorId by rememberSaveable(initCodeValue) { mutableStateOf(NATIVE_NULLPTR) }
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(if (initCodeValue == NATIVE_NULLPTR) {
|
||||
R.string.settings__theme_editor__add_code
|
||||
} else {
|
||||
R.string.settings__theme_editor__edit_code
|
||||
}),
|
||||
confirmLabel = stringRes(if (initCodeValue == NATIVE_NULLPTR) {
|
||||
R.string.action__add
|
||||
} else {
|
||||
R.string.action__apply
|
||||
}),
|
||||
onConfirm = {
|
||||
val code = inputCodeString.trim().toIntOrNull(radix = 10)
|
||||
when {
|
||||
code == null || (code !in KeyCode.Spec.CHARACTERS && code !in KeyCode.Spec.INTERNAL) -> {
|
||||
errorId = R.string.settings__theme_editor__code_invalid
|
||||
showError = true
|
||||
}
|
||||
code == initCodeValue -> {
|
||||
editCodeDialogValue = null
|
||||
}
|
||||
codes.contains(code) -> {
|
||||
errorId = R.string.settings__theme_editor__code_already_exists
|
||||
showError = true
|
||||
}
|
||||
else -> {
|
||||
if (initCodeValue != NATIVE_NULLPTR) {
|
||||
codes.remove(initCodeValue)
|
||||
}
|
||||
codes.add(code)
|
||||
editCodeDialogValue = null
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = {
|
||||
editCodeDialogValue = null
|
||||
},
|
||||
neutralLabel = if (initCodeValue != NATIVE_NULLPTR) {
|
||||
stringRes(R.string.action__delete)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
neutralColors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.error),
|
||||
onNeutral = {
|
||||
codes.remove(initCodeValue)
|
||||
editCodeDialogValue = null
|
||||
},
|
||||
trailingIconTitle = {
|
||||
FlorisIconButton(
|
||||
onClick = { showKeyCodesHelp = !showKeyCodesHelp },
|
||||
modifier = Modifier.offset(x = 12.dp),
|
||||
icon = painterResource(R.drawable.ic_help_outline),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(visible = showKeyCodesHelp) {
|
||||
Column(modifier = Modifier.padding(bottom = 16.dp)) {
|
||||
Text(text = stringRes(R.string.settings__theme_editor__code_help_text))
|
||||
FlorisHyperlinkText(
|
||||
text = "Characters (unicode-table.com)",
|
||||
url = stringRes(R.string.florisboard__character_key_codes_url),
|
||||
)
|
||||
FlorisHyperlinkText(
|
||||
text = "Internal (github.com)",
|
||||
url = stringRes(R.string.florisboard__internal_key_codes_url),
|
||||
)
|
||||
}
|
||||
}
|
||||
FlorisOutlinedTextField(
|
||||
value = inputCodeString,
|
||||
onValueChange = { v ->
|
||||
inputCodeString = v
|
||||
showError = false
|
||||
},
|
||||
isError = showError,
|
||||
singleLine = true,
|
||||
)
|
||||
AnimatedVisibility(visible = showError) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = stringRes(errorId).curlyFormat(
|
||||
"c_min" to KeyCode.Spec.CHARACTERS_MIN,
|
||||
"c_max" to KeyCode.Spec.CHARACTERS_MAX,
|
||||
"i_min" to KeyCode.Spec.INTERNAL_MIN,
|
||||
"i_max" to KeyCode.Spec.INTERNAL_MAX,
|
||||
),
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceLayout
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
|
||||
private val FineTuneContentPadding = PaddingValues(horizontal = 8.dp)
|
||||
|
||||
@Composable
|
||||
fun FineTuneDialog(onDismiss: () -> Unit) {
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(R.string.settings__theme_editor__fine_tune__title),
|
||||
onDismiss = onDismiss,
|
||||
contentPadding = FineTuneContentPadding,
|
||||
) {
|
||||
PreferenceLayout(florisPreferenceModel(), iconSpaceReserved = false) {
|
||||
ListPreference(
|
||||
listPref = prefs.theme.editorLevel,
|
||||
title = stringRes(R.string.settings__theme_editor__fine_tune__level),
|
||||
entries = SnyggLevel.listEntries(),
|
||||
)
|
||||
ListPreference(
|
||||
listPref = prefs.theme.editorDisplayColorsAs,
|
||||
title = stringRes(R.string.settings__theme_editor__fine_tune__display_colors_as),
|
||||
entries = DisplayColorsAs.listEntries(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDefinedVarValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValue
|
||||
|
||||
object SnyggValueIcon {
|
||||
interface Spec {
|
||||
val borderWith: Dp
|
||||
val boxShape: Shape
|
||||
val elevation: Dp
|
||||
val iconSize: Dp
|
||||
val iconSizeMinusBorder: Dp
|
||||
}
|
||||
|
||||
object Small : Spec {
|
||||
override val borderWith = Dp.Hairline
|
||||
override val boxShape = RoundedCornerShape(4.dp)
|
||||
override val elevation = 4.dp
|
||||
override val iconSize = 16.dp
|
||||
override val iconSizeMinusBorder = 16.dp
|
||||
}
|
||||
|
||||
object Normal : Spec {
|
||||
override val borderWith = 1.dp
|
||||
override val boxShape = RoundedCornerShape(8.dp)
|
||||
override val elevation = 4.dp
|
||||
override val iconSize = 24.dp
|
||||
override val iconSizeMinusBorder = 22.dp
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SnyggValueIcon(
|
||||
value: SnyggValue,
|
||||
definedVariables: Map<String, SnyggValue>,
|
||||
modifier: Modifier = Modifier,
|
||||
spec: SnyggValueIcon.Spec = SnyggValueIcon.Normal,
|
||||
) {
|
||||
when (value) {
|
||||
is SnyggSolidColorValue -> {
|
||||
Surface(
|
||||
modifier = modifier.requiredSize(spec.iconSize),
|
||||
color = MaterialTheme.colors.background,
|
||||
elevation = spec.elevation,
|
||||
shape = spec.boxShape,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(value.color),
|
||||
)
|
||||
}
|
||||
}
|
||||
is SnyggShapeValue -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.requiredSize(spec.iconSizeMinusBorder)
|
||||
.border(spec.borderWith, MaterialTheme.colors.onBackground, value.alwaysPercentShape())
|
||||
)
|
||||
}
|
||||
is SnyggDpSizeValue -> {
|
||||
Icon(
|
||||
modifier = modifier.requiredSize(spec.iconSize),
|
||||
painter = painterResource(R.drawable.ic_straighten),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
is SnyggSpSizeValue -> {
|
||||
Icon(
|
||||
modifier = modifier.requiredSize(spec.iconSize),
|
||||
painter = painterResource(R.drawable.ic_format_size),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
is SnyggDefinedVarValue -> {
|
||||
val realValue = definedVariables[value.key]
|
||||
if (realValue == null) {
|
||||
Icon(
|
||||
modifier = modifier.requiredSize(spec.iconSize),
|
||||
painter = painterResource(R.drawable.ic_link),
|
||||
contentDescription = null,
|
||||
)
|
||||
} else {
|
||||
val smallSpec = SnyggValueIcon.Small
|
||||
Box(modifier = modifier
|
||||
.requiredSize(spec.iconSize)
|
||||
.offset(y = (-2).dp)) {
|
||||
SnyggValueIcon(
|
||||
modifier = Modifier.offset(x = 8.dp, y = 8.dp),
|
||||
value = realValue,
|
||||
definedVariables = definedVariables,
|
||||
spec = smallSpec,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(x = 1.dp)
|
||||
.requiredSize(smallSpec.iconSize)
|
||||
.padding(vertical = 2.dp)
|
||||
.background(MaterialTheme.colors.background, spec.boxShape),
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.requiredSize(smallSpec.iconSize),
|
||||
painter = painterResource(R.drawable.ic_link),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val AlwaysPercentUpscaleFactor = 3
|
||||
|
||||
fun SnyggShapeValue.alwaysPercentShape(): Shape {
|
||||
return when (this) {
|
||||
is SnyggRoundedCornerDpShapeValue -> {
|
||||
RoundedCornerShape(
|
||||
this.topStart.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
this.topEnd.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
this.bottomEnd.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
this.bottomStart.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
)
|
||||
}
|
||||
is SnyggCutCornerDpShapeValue -> {
|
||||
CutCornerShape(
|
||||
this.topStart.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
this.topEnd.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
this.bottomEnd.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
this.bottomStart.value.toInt() * AlwaysPercentUpscaleFactor,
|
||||
)
|
||||
}
|
||||
else -> this.shape
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.ExtendedFloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedTextField
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.PreviewKeyboardField
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.florisVerticalScroll
|
||||
import dev.patrickgold.florisboard.app.ui.components.rememberPreviewFieldController
|
||||
import dev.patrickgold.florisboard.app.ui.components.rippleClickable
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionComponentView
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.common.rememberValidationResult
|
||||
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.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionValidation
|
||||
import dev.patrickgold.florisboard.res.io.readJson
|
||||
import dev.patrickgold.florisboard.res.io.subFile
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.snygg.SnyggPropertySetEditor
|
||||
import dev.patrickgold.florisboard.snygg.SnyggPropertySetSpec
|
||||
import dev.patrickgold.florisboard.snygg.SnyggRule
|
||||
import dev.patrickgold.florisboard.snygg.SnyggStylesheet
|
||||
import dev.patrickgold.florisboard.snygg.SnyggStylesheetEditor
|
||||
import dev.patrickgold.florisboard.snygg.SnyggStylesheetJsonConfig
|
||||
import dev.patrickgold.florisboard.snygg.definedVariablesRule
|
||||
import dev.patrickgold.florisboard.snygg.isDefinedVariablesRule
|
||||
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 kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal val IntListSaver = Saver<SnapshotStateList<Int>, ArrayList<Int>>(
|
||||
save = { ArrayList(it) },
|
||||
restore = { it.toMutableStateList() },
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ThemeEditorScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
editor: ThemeExtensionComponentEditor,
|
||||
) = FlorisScreen {
|
||||
title = stringRes(R.string.ext__editor__edit_component__title_theme)
|
||||
scrollable = false
|
||||
|
||||
val prefs by florisPreferenceModel()
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val themeManager by context.themeManager()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val previewFieldController = rememberPreviewFieldController().also { it.isVisible = true }
|
||||
|
||||
val stylesheetEditor = remember {
|
||||
editor.stylesheetEditor ?: run {
|
||||
val stylesheetPath = editor.stylesheetPath()
|
||||
editor.stylesheetPathOnLoad = stylesheetPath
|
||||
val stylesheetFile = workspace.extDir.subFile(stylesheetPath)
|
||||
val stylesheetEditor = if (stylesheetFile.exists()) {
|
||||
try {
|
||||
stylesheetFile.readJson<SnyggStylesheet>(SnyggStylesheetJsonConfig).edit()
|
||||
} catch (e: Throwable) {
|
||||
SnyggStylesheetEditor()
|
||||
}
|
||||
} else {
|
||||
SnyggStylesheetEditor()
|
||||
}
|
||||
if (stylesheetEditor.rules.none { (rule, _) -> rule.isDefinedVariablesRule() }) {
|
||||
stylesheetEditor.rules[SnyggRule.definedVariablesRule()] = SnyggPropertySetEditor()
|
||||
}
|
||||
stylesheetEditor
|
||||
}.also { editor.stylesheetEditor = it }
|
||||
}
|
||||
|
||||
val snyggLevel by prefs.theme.editorLevel.observeAsState()
|
||||
val displayColorsAs by prefs.theme.editorDisplayColorsAs.observeAsState()
|
||||
var snyggRuleToEdit by rememberSaveable(stateSaver = SnyggRule.Saver) { mutableStateOf(null) }
|
||||
var snyggPropertyToEdit by remember { mutableStateOf<PropertyInfo?>(null) }
|
||||
var snyggPropertySetForEditing = remember<SnyggPropertySetEditor?> { null }
|
||||
var snyggPropertySetSpecForEditing = remember<SnyggPropertySetSpec?> { null }
|
||||
var showEditComponentMetaDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showFineTuneDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
actions {
|
||||
FlorisIconButton(
|
||||
onClick = { showFineTuneDialog = true },
|
||||
icon = painterResource(R.drawable.ic_tune),
|
||||
)
|
||||
}
|
||||
|
||||
floatingActionButton {
|
||||
ExtendedFloatingActionButton(
|
||||
icon = { Icon(
|
||||
painter = painterResource(R.drawable.ic_add),
|
||||
contentDescription = null,
|
||||
) },
|
||||
text = { Text(
|
||||
text = stringRes(R.string.settings__theme_editor__add_rule),
|
||||
) },
|
||||
onClick = { snyggRuleToEdit = SnyggEmptyRuleForAdding },
|
||||
)
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
PreviewKeyboardField(previewFieldController)
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
LaunchedEffect(showEditComponentMetaDialog, showFineTuneDialog, snyggRuleToEdit, snyggPropertyToEdit) {
|
||||
val visible = showEditComponentMetaDialog || showFineTuneDialog ||
|
||||
snyggRuleToEdit != null || snyggPropertyToEdit != null
|
||||
if (visible) {
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
delay(250)
|
||||
previewFieldController.focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(workspace.version) {
|
||||
themeManager.previewThemeInfo = ThemeManager.ThemeInfo.DEFAULT.copy(
|
||||
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.
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
//modifier = Modifier.florisScrollbar(lazyListState, isVertical = true),
|
||||
state = lazyListState,
|
||||
) {
|
||||
item {
|
||||
Column {
|
||||
ExtensionComponentView(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
meta = workspace.editor!!.meta,
|
||||
component = editor,
|
||||
onEditBtnClick = { showEditComponentMetaDialog = true },
|
||||
)
|
||||
if (stylesheetEditor.rules.isEmpty() ||
|
||||
(stylesheetEditor.rules.size == 1 && stylesheetEditor.rules.keys.all { it.isDefinedVariablesRule() })
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
text = stringRes(R.string.settings__theme_editor__no_rules_defined),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
SnyggRuleRow(
|
||||
rule = rule,
|
||||
level = snyggLevel,
|
||||
showEditBtn = !isVariablesRule,
|
||||
onEditRuleBtnClick = {
|
||||
snyggRuleToEdit = rule
|
||||
},
|
||||
onAddPropertyBtnClick = {
|
||||
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_element__defines_description),
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} }
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(72.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (showEditComponentMetaDialog) {
|
||||
ComponentMetaEditorDialog(
|
||||
workspace = workspace,
|
||||
editor = editor,
|
||||
onConfirm = { showEditComponentMetaDialog = false },
|
||||
onDismiss = { showEditComponentMetaDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showFineTuneDialog) {
|
||||
FineTuneDialog(onDismiss = { showFineTuneDialog = false })
|
||||
}
|
||||
|
||||
val ruleToEdit = snyggRuleToEdit
|
||||
if (ruleToEdit != null) {
|
||||
EditRuleDialog(
|
||||
initRule = ruleToEdit,
|
||||
level = snyggLevel,
|
||||
onConfirmRule = { oldRule, newRule ->
|
||||
val rules = stylesheetEditor.rules
|
||||
when {
|
||||
oldRule == newRule -> {
|
||||
snyggRuleToEdit = null
|
||||
true
|
||||
}
|
||||
rules.contains(newRule) -> {
|
||||
false
|
||||
}
|
||||
else -> workspace.update {
|
||||
val set = rules.remove(oldRule)
|
||||
when {
|
||||
set != null -> {
|
||||
rules[newRule] = set
|
||||
snyggRuleToEdit = null
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(index = rules.keys.indexOf(newRule))
|
||||
}
|
||||
true
|
||||
}
|
||||
oldRule == SnyggEmptyRuleForAdding -> {
|
||||
rules[newRule] = SnyggPropertySetEditor()
|
||||
snyggRuleToEdit = null
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(index = rules.keys.indexOf(newRule))
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDeleteRule = { rule ->
|
||||
workspace.update {
|
||||
stylesheetEditor.rules.remove(rule)
|
||||
}
|
||||
snyggRuleToEdit = null
|
||||
},
|
||||
onDismiss = { snyggRuleToEdit = null },
|
||||
)
|
||||
}
|
||||
|
||||
val propertyToEdit = snyggPropertyToEdit
|
||||
if (propertyToEdit != null) {
|
||||
EditPropertyDialog(
|
||||
propertySetSpec = snyggPropertySetSpecForEditing,
|
||||
initProperty = propertyToEdit,
|
||||
level = snyggLevel,
|
||||
displayColorsAs = displayColorsAs,
|
||||
definedVariables = definedVariables,
|
||||
onConfirmNewValue = { name, value ->
|
||||
val properties = snyggPropertySetForEditing?.properties ?: return@EditPropertyDialog false
|
||||
if (propertyToEdit == SnyggEmptyPropertyInfoForAdding && properties.containsKey(name)) {
|
||||
return@EditPropertyDialog false
|
||||
}
|
||||
workspace.update {
|
||||
properties[name] = value
|
||||
}
|
||||
snyggPropertyToEdit = null
|
||||
true
|
||||
},
|
||||
onDelete = {
|
||||
workspace.update {
|
||||
snyggPropertySetForEditing?.properties?.remove(propertyToEdit.name)
|
||||
}
|
||||
snyggPropertyToEdit = null
|
||||
},
|
||||
onDismiss = { snyggPropertyToEdit = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentMetaEditorDialog(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
editor: ThemeExtensionComponentEditor,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var id by rememberSaveable { mutableStateOf(editor.id) }
|
||||
val idValidation = rememberValidationResult(ExtensionValidation.ComponentId, id)
|
||||
var label by rememberSaveable { mutableStateOf(editor.label) }
|
||||
val labelValidation = rememberValidationResult(ExtensionValidation.ComponentLabel, label)
|
||||
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)
|
||||
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(R.string.ext__editor__metadata__title),
|
||||
confirmLabel = stringRes(R.string.action__apply),
|
||||
onConfirm = {
|
||||
val allFieldsValid = idValidation.isValid() &&
|
||||
labelValidation.isValid() &&
|
||||
authorsValidation.isValid() &&
|
||||
stylesheetPathValidation.isValid()
|
||||
if (!allFieldsValid) {
|
||||
showValidationErrors = true
|
||||
} else if (id != editor.id && (workspace.editor as? ThemeExtensionEditor)?.themes?.find { it.id == id.trim() } != null) {
|
||||
context.showLongToast("A theme with this ID already exists!")
|
||||
} else {
|
||||
workspace.update {
|
||||
editor.id = id.trim()
|
||||
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()
|
||||
}
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = onDismiss,
|
||||
scrollModifier = Modifier.florisVerticalScroll(),
|
||||
) {
|
||||
Column {
|
||||
DialogProperty(text = stringRes(R.string.ext__meta__id)) {
|
||||
FlorisOutlinedTextField(
|
||||
value = id,
|
||||
onValueChange = { id = it },
|
||||
singleLine = true,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = idValidation,
|
||||
)
|
||||
}
|
||||
DialogProperty(text = stringRes(R.string.ext__meta__label)) {
|
||||
FlorisOutlinedTextField(
|
||||
value = label,
|
||||
onValueChange = { label = it },
|
||||
singleLine = true,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = labelValidation,
|
||||
)
|
||||
}
|
||||
DialogProperty(text = stringRes(R.string.ext__meta__authors)) {
|
||||
FlorisOutlinedTextField(
|
||||
value = authors,
|
||||
onValueChange = { authors = it },
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = authorsValidation,
|
||||
)
|
||||
}
|
||||
JetPrefListItem(
|
||||
modifier = Modifier.toggleable(isNightTheme) { isNightTheme = it },
|
||||
text = stringRes(R.string.settings__theme_editor__component_meta_is_night_theme),
|
||||
trailing = {
|
||||
Switch(checked = isNightTheme, onCheckedChange = null)
|
||||
},
|
||||
)
|
||||
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)) {
|
||||
FlorisOutlinedTextField(
|
||||
value = stylesheetPath,
|
||||
onValueChange = { stylesheetPath = it },
|
||||
singleLine = true,
|
||||
placeholder = if (stylesheetPath.isEmpty()) {
|
||||
ThemeExtensionComponent.defaultStylesheetPath(id.trim())
|
||||
} else {
|
||||
null
|
||||
},
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = stylesheetPathValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SnyggRuleRow(
|
||||
rule: SnyggRule,
|
||||
level: SnyggLevel,
|
||||
showEditBtn: Boolean,
|
||||
onEditRuleBtnClick: () -> Unit,
|
||||
onAddPropertyBtnClick: () -> Unit,
|
||||
) {
|
||||
@Composable
|
||||
fun Selector(text: String) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.background(MaterialTheme.colors.primaryVariant),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AttributesList(text: String, list: String) {
|
||||
Text(
|
||||
text = "$text = $list",
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = LocalContentColor.current.copy(alpha = 0.56f),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp, horizontal = 10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = translateElementName(rule, level),
|
||||
style = MaterialTheme.typography.body2,
|
||||
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)
|
||||
})
|
||||
}
|
||||
if (rule.focusSelector) {
|
||||
Selector(text = when (level) {
|
||||
SnyggLevel.DEVELOPER -> SnyggRule.FOCUS_SELECTOR
|
||||
else -> stringRes(R.string.snygg__rule_selector__focus)
|
||||
})
|
||||
}
|
||||
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 = "codes", list = remember(rule.codes) { rule.codes.toString() })
|
||||
}
|
||||
if (rule.modes.isNotEmpty()) {
|
||||
AttributesList(text = "modes", list = remember(rule.modes) { rule.modes.toString() })
|
||||
}
|
||||
}
|
||||
if (showEditBtn) {
|
||||
FlorisIconButton(
|
||||
onClick = onEditRuleBtnClick,
|
||||
icon = painterResource(R.drawable.ic_edit),
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
iconModifier = Modifier.size(ButtonDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
FlorisIconButton(
|
||||
onClick = onAddPropertyBtnClick,
|
||||
icon = painterResource(R.drawable.ic_add),
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
iconModifier = Modifier.size(ButtonDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DialogProperty(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
trailingIconTitle: @Composable () -> Unit = { },
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier.padding(bottom = 8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
)
|
||||
trailingIconTitle()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -47,12 +46,13 @@ import dev.patrickgold.florisboard.app.ui.components.FlorisConfirmDeleteDialog
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisTextButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.rippleClickable
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionImportScreenType
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.common.android.showShortToast
|
||||
import dev.patrickgold.florisboard.common.observeAsNonNullState
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
@@ -77,7 +77,7 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
ThemeManagerScreenAction.MANAGE -> R.string.settings__theme_manager__title_manage
|
||||
else -> error("Theme manager screen action must not be null")
|
||||
})
|
||||
previewFieldVisible = true
|
||||
previewFieldVisible = action != ThemeManagerScreenAction.MANAGE
|
||||
|
||||
val prefs by florisPreferenceModel()
|
||||
val navController = LocalNavController.current
|
||||
@@ -85,15 +85,12 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
val extensionManager by context.extensionManager()
|
||||
val themeManager by context.themeManager()
|
||||
|
||||
val indexedThemeConfigs by themeManager.indexedThemeConfigs.observeAsNonNullState()
|
||||
val indexedThemeExtensions by extensionManager.themes.observeAsNonNullState()
|
||||
val selectedManagerThemeId = remember { mutableStateOf<ExtensionComponentName?>(null) }
|
||||
val extGroupedThemes = remember(indexedThemeConfigs) {
|
||||
buildMap<String, MutableList<ThemeExtensionComponent>> {
|
||||
for ((componentName, config) in indexedThemeConfigs) {
|
||||
if (!containsKey(componentName.extensionId)) {
|
||||
put(componentName.extensionId, mutableListOf())
|
||||
}
|
||||
get(componentName.extensionId)!!.add(config)
|
||||
val extGroupedThemes = remember(indexedThemeExtensions) {
|
||||
buildMap<String, List<ThemeExtensionComponent>> {
|
||||
for (ext in indexedThemeExtensions) {
|
||||
put(ext.meta.id, ext.themes)
|
||||
}
|
||||
}.mapValues { (_, configs) -> configs.sortedBy { it.label } }
|
||||
}
|
||||
@@ -134,92 +131,84 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
val grayColor = LocalContentColor.current.copy(alpha = 0.56f)
|
||||
if (action == ThemeManagerScreenAction.MANAGE) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
) {
|
||||
Column {
|
||||
this@content.Preference(
|
||||
onClick = { context.showShortToast("TODO for 0.3.14-beta09") },
|
||||
iconId = R.drawable.ic_add,
|
||||
title = stringRes(R.string.ext__editor__create_new_extension),
|
||||
)
|
||||
this@content.Preference(
|
||||
onClick = { navController.navigate(
|
||||
Routes.Ext.Import(ExtensionImportScreenType.EXT_THEME, null)
|
||||
) },
|
||||
iconId = R.drawable.ic_input,
|
||||
title = stringRes(R.string.action__import),
|
||||
)
|
||||
}
|
||||
this@content.Preference(
|
||||
onClick = { navController.navigate(
|
||||
Routes.Ext.Edit("null", ThemeExtension.SERIAL_TYPE)
|
||||
) },
|
||||
iconId = R.drawable.ic_add,
|
||||
title = stringRes(R.string.ext__editor__title_create_theme),
|
||||
)
|
||||
this@content.Preference(
|
||||
onClick = { navController.navigate(
|
||||
Routes.Ext.Import(ExtensionImportScreenType.EXT_THEME, null)
|
||||
) },
|
||||
iconId = R.drawable.ic_input,
|
||||
title = stringRes(R.string.action__import),
|
||||
)
|
||||
}
|
||||
}
|
||||
for ((extensionId, configs) in extGroupedThemes) key(extensionId) {
|
||||
val ext = extensionManager.getExtensionById(extensionId)!!
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
title = remember {
|
||||
ext.meta.title
|
||||
},
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = ext.meta.title,
|
||||
onTitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
||||
subtitle = extensionId,
|
||||
onSubtitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
for (config in configs) key(extensionId, config.id) {
|
||||
JetPrefListItem(
|
||||
modifier = Modifier.rippleClickable {
|
||||
setTheme(extensionId, config.id)
|
||||
},
|
||||
icon = {
|
||||
RadioButton(
|
||||
selected = activeThemeId?.extensionId == extensionId &&
|
||||
activeThemeId?.componentId == config.id,
|
||||
onClick = null,
|
||||
)
|
||||
},
|
||||
text = config.label,
|
||||
trailing = {
|
||||
Icon(
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||
painter = painterResource(if (config.isNightTheme) {
|
||||
R.drawable.ic_dark_mode
|
||||
} else {
|
||||
R.drawable.ic_light_mode
|
||||
}),
|
||||
contentDescription = null,
|
||||
tint = grayColor,
|
||||
)
|
||||
for (config in configs) key(extensionId, config.id) {
|
||||
JetPrefListItem(
|
||||
modifier = Modifier.rippleClickable {
|
||||
setTheme(extensionId, config.id)
|
||||
},
|
||||
icon = {
|
||||
RadioButton(
|
||||
selected = activeThemeId?.extensionId == extensionId &&
|
||||
activeThemeId?.componentId == config.id,
|
||||
onClick = null,
|
||||
)
|
||||
},
|
||||
text = config.label,
|
||||
trailing = {
|
||||
Icon(
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||
painter = painterResource(if (config.isNightTheme) {
|
||||
R.drawable.ic_dark_mode
|
||||
} else {
|
||||
R.drawable.ic_light_mode
|
||||
}),
|
||||
contentDescription = null,
|
||||
tint = grayColor,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (action == ThemeManagerScreenAction.MANAGE && extensionManager.canDelete(ext)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp),
|
||||
) {
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
themeExtToDelete = ext
|
||||
},
|
||||
icon = painterResource(R.drawable.ic_delete),
|
||||
text = stringRes(R.string.action__delete),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colors.error,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
navController.navigate(Routes.Ext.Edit(ext.meta.id))
|
||||
},
|
||||
icon = painterResource(R.drawable.ic_edit),
|
||||
text = stringRes(R.string.action__edit),
|
||||
)
|
||||
}
|
||||
if (action == ThemeManagerScreenAction.MANAGE && extensionManager.canDelete(ext)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp),
|
||||
) {
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
themeExtToDelete = ext
|
||||
},
|
||||
icon = painterResource(R.drawable.ic_delete),
|
||||
text = stringRes(R.string.action__delete),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colors.error,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
/*TODO*/context.showShortToast("TODO for 0.3.14-beta09")
|
||||
},
|
||||
icon = painterResource(R.drawable.ic_edit),
|
||||
text = stringRes(R.string.action__edit),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,15 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
@@ -27,6 +32,7 @@ import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.Routes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisInfoCard
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.common.android.launchUrl
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
||||
@@ -41,22 +47,24 @@ fun ThemeScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__theme__title)
|
||||
previewFieldVisible = true
|
||||
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
content {
|
||||
val themeMode by prefs.theme.mode.observeAsState()
|
||||
val dayThemeId by prefs.theme.dayThemeId.observeAsState()
|
||||
val nightThemeId by prefs.theme.nightThemeId.observeAsState()
|
||||
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = """
|
||||
Themes can currently only be customized by writing (or modifying) a custom theme extension and then importing it, using the new FlexCSS stylesheet format packaged in a flex archive.
|
||||
|
||||
beta09 will provide a full in-app UI which allows to create and modify theme extensions hassle-free in a modern theme editor UI.
|
||||
|
||||
Additionally the theme mode "Follow time" is not available in this beta release.
|
||||
""".trimIndent()
|
||||
)
|
||||
Card(modifier = Modifier.padding(8.dp)) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Text("If you want to give feedback on the new stylesheet editor and theme engine, please do so in below linked feedback thread:\n")
|
||||
Button(onClick = {
|
||||
context.launchUrl("https://github.com/florisboard/florisboard/discussions/1531")
|
||||
}) {
|
||||
Text("Open Feedback Thread")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListPreference(
|
||||
prefs.theme.mode,
|
||||
@@ -64,6 +72,14 @@ fun ThemeScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.pref__theme__mode__label),
|
||||
entries = ThemeMode.listEntries(),
|
||||
)
|
||||
if (themeMode == ThemeMode.FOLLOW_TIME) {
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = """
|
||||
The theme mode "Follow time" is not available in this beta release.
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
Preference(
|
||||
iconId = R.drawable.ic_palette,
|
||||
title = stringRes(R.string.settings__theme_manager__title_manage),
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.settings.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.snygg.Snygg
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.snygg.SnyggRule
|
||||
import dev.patrickgold.florisboard.snygg.value.RgbaColor
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCircleShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDefinedVarValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggExplicitInheritValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggImplicitInheritValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggPercentageSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRectangleShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValueEncoder
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun translateElementName(element: String, level: SnyggLevel): String? {
|
||||
return when (level) {
|
||||
SnyggLevel.DEVELOPER -> null
|
||||
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.GlideTrail -> R.string.snygg__rule_element__glide_trail
|
||||
FlorisImeUi.OneHandedPanel -> R.string.snygg__rule_element__one_handed_panel
|
||||
FlorisImeUi.SmartbarPrimaryRow -> R.string.snygg__rule_element__smartbar_primary_row
|
||||
FlorisImeUi.SmartbarPrimaryActionRowToggle -> R.string.snygg__rule_element__smartbar_primary_action_row_toggle
|
||||
FlorisImeUi.SmartbarPrimarySecondaryRowToggle -> R.string.snygg__rule_element__smartbar_primary_secondary_row_toggle
|
||||
FlorisImeUi.SmartbarSecondaryRow -> R.string.snygg__rule_element__smartbar_secondary_row
|
||||
FlorisImeUi.SmartbarActionRow -> R.string.snygg__rule_element__smartbar_action_row
|
||||
FlorisImeUi.SmartbarActionButton -> R.string.snygg__rule_element__smartbar_action_button
|
||||
FlorisImeUi.SmartbarCandidateRow -> R.string.snygg__rule_element__smartbar_candidate_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.SmartbarKey -> R.string.snygg__rule_element__smartbar_key
|
||||
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.Border -> R.string.snygg__property_name__border
|
||||
Snygg.BorderTop -> R.string.snygg__property_name__border_top
|
||||
Snygg.BorderBottom -> R.string.snygg__property_name__border_bottom
|
||||
Snygg.BorderStart -> R.string.snygg__property_name__border_start
|
||||
Snygg.BorderEnd -> R.string.snygg__property_name__border_end
|
||||
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 -> {
|
||||
stringRes(resId)
|
||||
}
|
||||
propertyName.isBlank() -> {
|
||||
stringRes(R.string.general__select_dropdown_value_placeholder)
|
||||
}
|
||||
else -> {
|
||||
propertyName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun translatePropertyValue(
|
||||
propertyValue: SnyggValue,
|
||||
level: SnyggLevel,
|
||||
displayColorsAs: DisplayColorsAs,
|
||||
): String {
|
||||
return when (propertyValue) {
|
||||
is SnyggSolidColorValue -> remember(propertyValue.color, displayColorsAs) {
|
||||
val color = propertyValue.color
|
||||
when (displayColorsAs) {
|
||||
DisplayColorsAs.HEX8 -> buildString {
|
||||
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))
|
||||
}
|
||||
DisplayColorsAs.RGBA -> buildString {
|
||||
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(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> when (level) {
|
||||
SnyggLevel.DEVELOPER -> null
|
||||
else -> when (propertyValue) {
|
||||
is SnyggDefinedVarValue -> translatePropertyName(propertyValue.key, level)
|
||||
else -> null
|
||||
}
|
||||
} ?: propertyValue.encoder().serialize(propertyValue).getOrElse { propertyValue.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
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()
|
||||
}
|
||||
@@ -61,7 +61,7 @@ private object Step {
|
||||
@Composable
|
||||
fun SetupScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.setup__title)
|
||||
backArrowVisible = false
|
||||
navigationIconVisible = false
|
||||
scrollable = false
|
||||
|
||||
val navController = LocalNavController.current
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package dev.patrickgold.florisboard.app.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
@@ -85,3 +87,7 @@ fun FlorisAppTheme(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
val Colors.outline: Color
|
||||
@Composable
|
||||
get() = this.onSurface.copy(alpha = ButtonDefaults.OutlinedBorderOpacity)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.common
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.common.kotlin.CurlyArg
|
||||
import dev.patrickgold.florisboard.common.kotlin.curlyFormat
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
sealed class ValidationResult {
|
||||
companion object {
|
||||
fun resultValid(): ValidationResult {
|
||||
return Valid()
|
||||
}
|
||||
|
||||
fun resultValid(@StringRes hint: Int): ValidationResult {
|
||||
return Valid(hintMessageId = hint)
|
||||
}
|
||||
|
||||
fun resultValid(hint: String): ValidationResult {
|
||||
return Valid(hintMessageStr = hint)
|
||||
}
|
||||
|
||||
fun resultInvalid(@StringRes error: Int): ValidationResult {
|
||||
return Invalid(errorMessageId = error)
|
||||
}
|
||||
|
||||
fun resultInvalid(error: String): ValidationResult {
|
||||
return Invalid(errorMessageStr = error)
|
||||
}
|
||||
}
|
||||
|
||||
data class Valid(
|
||||
@StringRes private val hintMessageId: Int? = null,
|
||||
private val hintMessageStr: String? = null,
|
||||
private val args: List<CurlyArg> = emptyList(),
|
||||
) : ValidationResult() {
|
||||
|
||||
fun hasHintMessage(): Boolean {
|
||||
return hintMessageId != null || hintMessageStr != null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun hintMessage(): String {
|
||||
return when {
|
||||
hintMessageId != null -> stringRes(hintMessageId).curlyFormat(args)
|
||||
hintMessageStr != null -> hintMessageStr.curlyFormat(args)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Invalid(
|
||||
@StringRes private val errorMessageId: Int? = null,
|
||||
private val errorMessageStr: String? = null,
|
||||
private val args: List<CurlyArg> = emptyList(),
|
||||
) : ValidationResult() {
|
||||
|
||||
fun hasErrorMessage(): Boolean {
|
||||
return errorMessageId != null || errorMessageStr != null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun errorMessage(): String {
|
||||
return when {
|
||||
errorMessageId != null -> stringRes(errorMessageId).curlyFormat(args)
|
||||
errorMessageStr != null -> errorMessageStr.curlyFormat(args)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isValid(): Boolean {
|
||||
contract {
|
||||
returns(true) implies (this@ValidationResult is Valid)
|
||||
}
|
||||
return this is Valid
|
||||
}
|
||||
|
||||
fun isInvalid(): Boolean {
|
||||
contract {
|
||||
returns(true) implies (this@ValidationResult is Invalid)
|
||||
}
|
||||
return this is Invalid
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T : Any> rememberValidationResult(rule: ValidationRule<T>, value: T): ValidationResult {
|
||||
return remember(value) {
|
||||
rule.validator.invoke(ValidationResult.Companion, value)
|
||||
}
|
||||
}
|
||||
|
||||
data class ValidationRule<T : Any>(
|
||||
val klass: KClass<*>,
|
||||
val propertyName: String,
|
||||
val validator: ValidationResult.Companion.(T) -> ValidationResult,
|
||||
)
|
||||
|
||||
class ValidationRuleBuilder<T : Any> {
|
||||
var forKlass: KClass<*>? = null
|
||||
var forProperty: String? = null
|
||||
|
||||
private var validator: (ValidationResult.Companion.(T) -> ValidationResult)? = null
|
||||
fun validator(validator: ValidationResult.Companion.(T) -> ValidationResult) {
|
||||
this.validator = validator
|
||||
}
|
||||
|
||||
fun build() = ValidationRule(forKlass!!, forProperty!!, validator!!)
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun <T : Any> ValidationRule(scope: ValidationRuleBuilder<T>.() -> Unit): ValidationRule<T> {
|
||||
val builder = ValidationRuleBuilder<T>()
|
||||
scope(builder)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun <T : Any> validate(rule: ValidationRule<T>, value: T): ValidationResult {
|
||||
return rule.validator.invoke(ValidationResult.Companion, value)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
* 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,15 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
package dev.patrickgold.florisboard.common.kotlin
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
@Composable
|
||||
inline fun Extension.rememberComponents(): List<ExtensionComponent> {
|
||||
return remember(this) { this.components() }
|
||||
inline fun <R> tryOrNull(block: () -> R): R? {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return try {
|
||||
block()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,10 @@ fun String.curlyFormat(argValueFactory: (argName: String) -> String?): String {
|
||||
}
|
||||
|
||||
fun String.curlyFormat(vararg args: CurlyArg): String {
|
||||
return this.curlyFormat(args.asList())
|
||||
}
|
||||
|
||||
fun String.curlyFormat(args: List<CurlyArg>): String {
|
||||
if (args.isEmpty()) return this
|
||||
val sb = StringBuilder(this)
|
||||
for ((n, arg) in args.withIndex()) {
|
||||
|
||||
@@ -30,11 +30,25 @@ import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.prefs.AppPrefs
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.debug.*
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
private class SafePreferenceInstanceWrapper : ReadOnlyProperty<Any?, AppPrefs?> {
|
||||
val cachedPreferenceModel = try {
|
||||
florisPreferenceModel()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): AppPrefs? {
|
||||
return cachedPreferenceModel?.getValue(thisRef, property)
|
||||
}
|
||||
}
|
||||
|
||||
class CrashDialogActivity : ComponentActivity() {
|
||||
private var stacktraces: List<CrashUtility.Stacktrace> = listOf()
|
||||
private var errorReport: StringBuilder = StringBuilder()
|
||||
private var prefs: AppPrefs? = null
|
||||
private val prefs by SafePreferenceInstanceWrapper()
|
||||
|
||||
private val stacktrace by lazy { findViewById<TextView>(R.id.stacktrace) }
|
||||
private val reportInstructions by lazy { findViewById<TextView>(R.id.report_instructions) }
|
||||
@@ -48,13 +62,6 @@ class CrashDialogActivity : ComponentActivity() {
|
||||
val layout = layoutInflater.inflate(R.layout.crash_dialog, null)
|
||||
setContentView(layout)
|
||||
|
||||
// We secure the PrefHelper usage here because the PrefHelper could potentially be the
|
||||
// source of the crash, thus making the crash dialog unusable if not wrapped.
|
||||
try {
|
||||
prefs = florisPreferenceModel().preferenceModel
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
stacktraces = CrashUtility.getUnhandledStacktraces(this)
|
||||
errorReport.apply {
|
||||
appendLine("#### Environment information")
|
||||
|
||||
@@ -29,7 +29,10 @@ import dev.patrickgold.florisboard.FlorisImeService
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.debug.*
|
||||
import java.io.File
|
||||
import dev.patrickgold.florisboard.res.io.FsDir
|
||||
import dev.patrickgold.florisboard.res.io.FsFile
|
||||
import dev.patrickgold.florisboard.res.io.subDir
|
||||
import dev.patrickgold.florisboard.res.io.subFile
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@@ -49,6 +52,7 @@ abstract class CrashUtility private constructor() {
|
||||
private const val NOTIFICATION_CHANNEL_ID = "${BuildConfig.APPLICATION_ID}.crashutility"
|
||||
private const val NOTIFICATION_ID = 0xFBAD0100
|
||||
|
||||
private const val UNHANDLED_STACKTRACES_DIR_NAME = "unhandled_stacktraces"
|
||||
private const val UNHANDLED_STACKTRACE_FILE_EXT = "stacktrace"
|
||||
|
||||
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
|
||||
@@ -82,7 +86,7 @@ abstract class CrashUtility private constructor() {
|
||||
UncaughtExceptionHandler(
|
||||
WeakReference(application),
|
||||
WeakReference(oldHandler),
|
||||
application.filesDir.absolutePath
|
||||
context.getUstDir(),
|
||||
)
|
||||
)
|
||||
flogInfo(LogTopic.CRASH_UTILITY) {
|
||||
@@ -174,7 +178,7 @@ abstract class CrashUtility private constructor() {
|
||||
fun getUnhandledStacktraces(context: Context?): List<Stacktrace> {
|
||||
context ?: return listOf()
|
||||
val retList = mutableListOf<Stacktrace>()
|
||||
val ustDir = getUstDir(context)
|
||||
val ustDir = context.getUstDir()
|
||||
if (ustDir.isDirectory) {
|
||||
(ustDir.listFiles { pathname ->
|
||||
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
@@ -190,7 +194,7 @@ abstract class CrashUtility private constructor() {
|
||||
}
|
||||
|
||||
fun hasUnhandledStacktraceFiles(context: Context): Boolean {
|
||||
val ustDir = getUstDir(context)
|
||||
val ustDir = context.getUstDir()
|
||||
return if (ustDir.isDirectory) {
|
||||
(ustDir.listFiles { pathname ->
|
||||
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
@@ -234,24 +238,20 @@ abstract class CrashUtility private constructor() {
|
||||
/**
|
||||
* Gets a reference to the current unhandled stacktrace directory.
|
||||
*
|
||||
* @param context The current package context.
|
||||
* @return The File object for the directory.
|
||||
*/
|
||||
private fun getUstDir(context: Context): File {
|
||||
val path = context.filesDir.absolutePath
|
||||
return File(path)
|
||||
private fun Context.getUstDir(): FsDir {
|
||||
return this.noBackupFilesDir.subDir(UNHANDLED_STACKTRACES_DIR_NAME).also { it.mkdirs() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to the stacktrace file for given [timestamp].
|
||||
*
|
||||
* @param context The current package context.
|
||||
* @param timestamp The timestamp of the stacktrace file to get.
|
||||
* @return The File object for the stacktrace file.
|
||||
*/
|
||||
private fun getUstFile(context: Context, timestamp: Long): File {
|
||||
val path = context.filesDir.absolutePath
|
||||
return File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
private fun Context.getUstFile(timestamp: Long): FsFile {
|
||||
return this.getUstDir().subFile("$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,7 +328,7 @@ abstract class CrashUtility private constructor() {
|
||||
* @param file The file object.
|
||||
* @return The contents of the file or an empty string, if the file does not exist.
|
||||
*/
|
||||
private fun readFile(file: File): String {
|
||||
private fun readFile(file: FsFile): String {
|
||||
val retText = StringBuilder()
|
||||
if (file.exists()) {
|
||||
val newLine = System.lineSeparator()
|
||||
@@ -348,7 +348,7 @@ abstract class CrashUtility private constructor() {
|
||||
* @param text The text to write to the file.
|
||||
* @return The contents of the file or an empty string, if the file does not exist.
|
||||
*/
|
||||
private fun writeToFile(file: File, text: String) {
|
||||
private fun writeToFile(file: FsFile, text: String) {
|
||||
try {
|
||||
file.writeText(text)
|
||||
} catch (e: Exception) {
|
||||
@@ -372,7 +372,7 @@ abstract class CrashUtility private constructor() {
|
||||
class UncaughtExceptionHandler(
|
||||
private val application: WeakReference<Application>,
|
||||
private val oldHandler: WeakReference<Thread.UncaughtExceptionHandler?>,
|
||||
private val path: String
|
||||
private val ustDir: FsDir,
|
||||
) : Thread.UncaughtExceptionHandler {
|
||||
override fun uncaughtException(thread: Thread?, throwable: Throwable?) {
|
||||
flogInfo(LogTopic.CRASH_UTILITY) {
|
||||
@@ -381,13 +381,13 @@ abstract class CrashUtility private constructor() {
|
||||
throwable ?: return
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val stacktrace = Log.getStackTraceString(throwable)
|
||||
val ustFile = File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
val ustFile = ustDir.subFile("$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
writeToFile(ustFile, stacktrace)
|
||||
val application = application.get()
|
||||
if (application != null) {
|
||||
val lastTimestamp = getLastCrashTimestamp(application)
|
||||
if (lastTimestamp > 0) {
|
||||
val lastFile = getUstFile(application, lastTimestamp)
|
||||
val lastFile = application.getUstFile(lastTimestamp)
|
||||
val lastStacktrace = readFile(lastFile)
|
||||
if (lastStacktrace == stacktrace) {
|
||||
// Delete last stacktrace if it matches previous unhandled one
|
||||
|
||||
@@ -59,10 +59,11 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButtonWithInnerPadding
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisStaggeredVerticalGrid
|
||||
import dev.patrickgold.florisboard.app.ui.components.florisVerticalScroll
|
||||
import dev.patrickgold.florisboard.app.ui.components.rippleClickable
|
||||
import dev.patrickgold.florisboard.app.ui.components.safeTimes
|
||||
import dev.patrickgold.florisboard.app.ui.theme.Green500
|
||||
import dev.patrickgold.florisboard.clipboardManager
|
||||
import dev.patrickgold.florisboard.common.android.AndroidKeyguardManager
|
||||
@@ -80,6 +81,7 @@ import dev.patrickgold.florisboard.snygg.SnyggPropertySet
|
||||
import dev.patrickgold.florisboard.snygg.ui.SnyggSurface
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggBackground
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggClip
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggShadow
|
||||
import dev.patrickgold.florisboard.snygg.ui.solidColor
|
||||
import dev.patrickgold.florisboard.snygg.ui.spSize
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
@@ -132,7 +134,7 @@ fun ClipboardInputLayout(
|
||||
.snyggBackground(headerStyle.background),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
FlorisIconButton(
|
||||
FlorisIconButtonWithInnerPadding(
|
||||
onClick = { activeState.imeUiMode = ImeUiMode.TEXT },
|
||||
modifier = Modifier
|
||||
.padding(HeaderIconPadding)
|
||||
@@ -147,7 +149,7 @@ fun ClipboardInputLayout(
|
||||
color = headerStyle.foreground.solidColor(),
|
||||
fontSize = headerStyle.fontSize.spSize(),
|
||||
)
|
||||
FlorisIconButton(
|
||||
FlorisIconButtonWithInnerPadding(
|
||||
onClick = { prefs.clipboard.historyEnabled.set(!historyEnabled) },
|
||||
modifier = Modifier
|
||||
.padding(HeaderIconPadding)
|
||||
@@ -161,7 +163,7 @@ fun ClipboardInputLayout(
|
||||
iconColor = headerStyle.foreground.solidColor(),
|
||||
enabled = !deviceLocked && popupItem == null,
|
||||
)
|
||||
FlorisIconButton(
|
||||
FlorisIconButtonWithInnerPadding(
|
||||
onClick = {
|
||||
clipboardManager.clearHistory()
|
||||
context.showShortToast(R.string.clipboard__cleared_history)
|
||||
@@ -174,7 +176,7 @@ fun ClipboardInputLayout(
|
||||
iconColor = headerStyle.foreground.solidColor(),
|
||||
enabled = !deviceLocked && historyEnabled && popupItem == null,
|
||||
)
|
||||
FlorisIconButton(
|
||||
FlorisIconButtonWithInnerPadding(
|
||||
onClick = {
|
||||
context.showShortToast("TODO: implement inline clip item editing")
|
||||
},
|
||||
@@ -201,6 +203,7 @@ fun ClipboardInputLayout(
|
||||
.fillMaxWidth()
|
||||
.padding(ItemMargin),
|
||||
background = style.background,
|
||||
elevation = style.shadowElevation,
|
||||
shape = style.shape,
|
||||
clip = true,
|
||||
contentPadding = ItemPadding,
|
||||
@@ -292,6 +295,7 @@ fun ClipboardInputLayout(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(ItemMargin)
|
||||
.snyggShadow(popupStyle)
|
||||
.snyggBackground(popupStyle.background, popupStyle.shape)
|
||||
.snyggClip(popupStyle.shape),
|
||||
) {
|
||||
@@ -346,7 +350,7 @@ fun ClipboardInputLayout(
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp),
|
||||
text = stringRes(R.string.clipboard__empty__title),
|
||||
color = itemStyle.foreground.solidColor(),
|
||||
fontSize = itemStyle.fontSize.spSize() * 1.1f,
|
||||
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
@@ -381,7 +385,7 @@ fun ClipboardInputLayout(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
text = stringRes(R.string.clipboard__disabled__title),
|
||||
color = itemStyle.foreground.solidColor(),
|
||||
fontSize = itemStyle.fontSize.spSize() * 1.1f,
|
||||
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
@@ -422,7 +426,7 @@ fun ClipboardInputLayout(
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp),
|
||||
text = stringRes(R.string.clipboard__locked__title),
|
||||
color = itemStyle.foreground.solidColor(),
|
||||
fontSize = itemStyle.fontSize.spSize() * 1.1f,
|
||||
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
@@ -470,7 +474,7 @@ private fun ClipCategoryTitle(
|
||||
text = text.uppercase(),
|
||||
color = style.foreground.solidColor(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = style.fontSize.spSize() * 0.8f,
|
||||
fontSize = style.fontSize.spSize() safeTimes 0.8f,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
private const val SERIAL_TYPE = "ime.extension.keyboard"
|
||||
|
||||
@SerialName(SERIAL_TYPE)
|
||||
@SerialName(KeyboardExtension.SERIAL_TYPE)
|
||||
@Serializable
|
||||
data class KeyboardExtension(
|
||||
override val meta: ExtensionMeta,
|
||||
@@ -39,6 +37,10 @@ data class KeyboardExtension(
|
||||
val subtypePresets: List<SubtypePreset> = listOf(),
|
||||
) : Extension() {
|
||||
|
||||
companion object {
|
||||
const val SERIAL_TYPE = "ime.extension.keyboard"
|
||||
}
|
||||
|
||||
override fun serialType() = SERIAL_TYPE
|
||||
|
||||
override fun components(): List<ExtensionComponent> {
|
||||
|
||||
@@ -474,8 +474,17 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
|
||||
* enabled by the user.
|
||||
*/
|
||||
private fun handleSpace(ev: InputKeyEvent) = activeEditorInstance?.apply {
|
||||
if (prefs.keyboard.spaceBarSwitchesToCharacters.get() && activeState.keyboardMode != KeyboardMode.CHARACTERS) {
|
||||
activeState.keyboardMode = KeyboardMode.CHARACTERS
|
||||
if (prefs.keyboard.spaceBarSwitchesToCharacters.get()) {
|
||||
when (activeState.keyboardMode) {
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
KeyboardMode.SYMBOLS,
|
||||
KeyboardMode.SYMBOLS2 -> {
|
||||
activeState.keyboardMode = KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prefs.correction.doubleSpacePeriod.get()) {
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventUp, prefs.keyboard.longPressDelay.get().toLong())) {
|
||||
|
||||
@@ -34,12 +34,14 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.ui.components.safeTimes
|
||||
import dev.patrickgold.florisboard.ime.keyboard.Key
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.snygg.ui.SnyggSurface
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggBackground
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggShadow
|
||||
import dev.patrickgold.florisboard.snygg.ui.solidColor
|
||||
import dev.patrickgold.florisboard.snygg.ui.spSize
|
||||
|
||||
@@ -53,10 +55,11 @@ fun PopupBaseBox(
|
||||
val popupStyle = FlorisImeTheme.style.get(
|
||||
element = FlorisImeUi.KeyPopup,
|
||||
)
|
||||
val fontSize = popupStyle.fontSize.spSize() * fontSizeMultiplier
|
||||
val fontSize = popupStyle.fontSize.spSize() safeTimes fontSizeMultiplier
|
||||
SnyggSurface(
|
||||
modifier = modifier,
|
||||
background = popupStyle.background,
|
||||
elevation = popupStyle.shadowElevation,
|
||||
shape = popupStyle.shape,
|
||||
clip = true,
|
||||
) {
|
||||
@@ -106,6 +109,7 @@ fun PopupExtBox(
|
||||
)
|
||||
Column(
|
||||
modifier = modifier
|
||||
.snyggShadow(popupStyle)
|
||||
.snyggBackground(popupStyle.background, popupStyle.shape),
|
||||
) {
|
||||
for (row in elements.asReversed()) {
|
||||
@@ -124,7 +128,7 @@ fun PopupExtBox(
|
||||
} else {
|
||||
popupStyle
|
||||
}
|
||||
val elemFontSize = elemStyle.fontSize.spSize() * fontSizeMultiplier *
|
||||
val elemFontSize = elemStyle.fontSize.spSize() safeTimes fontSizeMultiplier safeTimes
|
||||
if (element.data.code == KeyCode.URI_COMPONENT_TLD) { 0.6f } else { 1.0f }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -22,15 +22,12 @@ import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMetaEditor
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import java.io.File
|
||||
|
||||
private const val SERIAL_TYPE = "ime.extension.spelling"
|
||||
|
||||
@SerialName(SERIAL_TYPE)
|
||||
@SerialName(SpellingExtension.SERIAL_TYPE)
|
||||
@Serializable
|
||||
data class SpellingExtension(
|
||||
override val meta: ExtensionMeta,
|
||||
@@ -38,6 +35,10 @@ data class SpellingExtension(
|
||||
val spelling: SpellingExtensionConfig,
|
||||
) : Extension() {
|
||||
|
||||
companion object {
|
||||
const val SERIAL_TYPE = "ime.extension.spelling"
|
||||
}
|
||||
|
||||
@Transient var dict: SpellingDict? = null
|
||||
|
||||
override fun serialType() = SERIAL_TYPE
|
||||
@@ -56,21 +57,23 @@ data class SpellingExtension(
|
||||
}
|
||||
|
||||
override fun edit() = SpellingExtensionEditor(
|
||||
meta.edit(),
|
||||
meta = meta,
|
||||
dependencies?.toMutableList() ?: mutableListOf(),
|
||||
workingDir,
|
||||
spelling.edit(),
|
||||
)
|
||||
}
|
||||
|
||||
data class SpellingExtensionEditor(
|
||||
override val meta: ExtensionMetaEditor,
|
||||
override var workingDir: File?,
|
||||
override var meta: ExtensionMeta,
|
||||
override val dependencies: MutableList<String>,
|
||||
var workingDir: File?,
|
||||
val spelling: SpellingExtensionConfigEditor,
|
||||
) : ExtensionEditor {
|
||||
fun build() = runCatching {
|
||||
SpellingExtension(
|
||||
meta = meta.build().getOrThrow(),
|
||||
spelling = spelling.build().getOrThrow(),
|
||||
override fun build(): SpellingExtension {
|
||||
return SpellingExtension(
|
||||
meta = meta,
|
||||
spelling = spelling.build(),
|
||||
).also {
|
||||
it.workingDir = workingDir
|
||||
}
|
||||
@@ -97,7 +100,7 @@ data class SpellingExtensionConfigEditor(
|
||||
var affFile: String = "",
|
||||
var dicFile: String = "",
|
||||
) {
|
||||
fun build() = runCatching {
|
||||
fun build(): SpellingExtensionConfig {
|
||||
val config = SpellingExtensionConfig(
|
||||
locale.trim().let { FlorisLocale.from(it) },
|
||||
originalSourceId.trim().ifBlank { null },
|
||||
@@ -106,6 +109,6 @@ data class SpellingExtensionConfigEditor(
|
||||
)
|
||||
check(config.affFile.isNotBlank()) { "Spelling extension aff file path cannot be blank" }
|
||||
check(config.dicFile.isNotBlank()) { "Spelling extension dic file path cannot be blank" }
|
||||
return@runCatching config
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,9 @@ import dev.patrickgold.florisboard.debug.flogError
|
||||
import dev.patrickgold.florisboard.debug.flogInfo
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.res.ExternalContentUtils
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMaintainer
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMaintainerEditor
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionDefaults
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMetaEditor
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMaintainer
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
@@ -200,15 +199,16 @@ class SpellingManager(context: Context) {
|
||||
tempDictDir.mkdirs()
|
||||
val entries = zipFile.entries()
|
||||
val extensionEditor = SpellingExtensionEditor(
|
||||
meta = ExtensionMetaEditor(
|
||||
meta = ExtensionMeta(
|
||||
id = ExtensionDefaults.createLocalId("spelling"),
|
||||
version = manifest.version ?: "0.0.0",
|
||||
title = manifest.name ?: "Imported spelling dict",
|
||||
description = manifest.description ?: "",
|
||||
maintainers = manifest.author?.let { mutableListOf(ExtensionMaintainer.fromOrTakeRaw(it).edit()) }
|
||||
?: mutableListOf(ExtensionMaintainerEditor(name = "Unknown")),
|
||||
maintainers = manifest.author?.let { mutableListOf(ExtensionMaintainer.fromOrTakeRaw(it)) }
|
||||
?: mutableListOf(ExtensionMaintainer(name = "Unknown")),
|
||||
license = "unknown",
|
||||
),
|
||||
dependencies = mutableListOf(),
|
||||
workingDir = tempDictDir,
|
||||
spelling = SpellingExtensionConfigEditor().apply {
|
||||
locale = supportedLocale.languageTag()
|
||||
@@ -275,13 +275,14 @@ class SpellingManager(context: Context) {
|
||||
tempDictDir.mkdirs()
|
||||
val entries = zipFile.entries()
|
||||
val extensionEditor = SpellingExtensionEditor(
|
||||
meta = ExtensionMetaEditor(
|
||||
meta = ExtensionMeta(
|
||||
id = ExtensionDefaults.createLocalId("spelling"),
|
||||
version = "0.0.0",
|
||||
title = "FreeOffice import",
|
||||
maintainers = mutableListOf(ExtensionMaintainerEditor(name = "Unknown")),
|
||||
maintainers = mutableListOf(ExtensionMaintainer(name = "Unknown")),
|
||||
license = "unknown",
|
||||
),
|
||||
dependencies = mutableListOf(),
|
||||
workingDir = tempDictDir,
|
||||
spelling = SpellingExtensionConfigEditor().apply {
|
||||
locale = supportedLocale!!.languageTag()
|
||||
|
||||
@@ -42,6 +42,8 @@ enum class SwipeAction {
|
||||
MOVE_CURSOR_START_OF_PAGE,
|
||||
MOVE_CURSOR_END_OF_PAGE,
|
||||
REDO,
|
||||
SELECT_CHARACTERS_PRECISELY,
|
||||
SELECT_WORDS_PRECISELY,
|
||||
SHIFT,
|
||||
SHOW_INPUT_METHOD_PICKER,
|
||||
SWITCH_TO_PREV_SUBTYPE,
|
||||
@@ -161,6 +163,14 @@ enum class SwipeAction {
|
||||
key = DELETE_WORDS_PRECISELY,
|
||||
label = stringRes(R.string.enum__swipe_action__delete_words_precisely),
|
||||
)
|
||||
entry(
|
||||
key = SELECT_CHARACTERS_PRECISELY,
|
||||
label = stringRes(R.string.enum__swipe_action__select_characters_precisely),
|
||||
)
|
||||
entry(
|
||||
key = SELECT_WORDS_PRECISELY,
|
||||
label = stringRes(R.string.enum__swipe_action__select_words_precisely),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
object KeyCode {
|
||||
object Spec {
|
||||
const val CHARACTERS_MIN = 1
|
||||
const val CHARACTERS_MAX = 65535
|
||||
val CHARACTERS = CHARACTERS_MIN..CHARACTERS_MAX
|
||||
|
||||
const val INTERNAL_MIN = -9999
|
||||
const val INTERNAL_MAX = -1
|
||||
val INTERNAL = INTERNAL_MIN..INTERNAL_MAX
|
||||
}
|
||||
|
||||
const val UNSPECIFIED = 0
|
||||
|
||||
const val PHONE_WAIT = 59 // ;
|
||||
|
||||
@@ -58,6 +58,7 @@ import androidx.compose.ui.unit.toSize
|
||||
import dev.patrickgold.florisboard.FlorisImeService
|
||||
import dev.patrickgold.florisboard.app.prefs.AppPrefs
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.app.ui.components.safeTimes
|
||||
import dev.patrickgold.florisboard.common.FlorisRect
|
||||
import dev.patrickgold.florisboard.common.Pointer
|
||||
import dev.patrickgold.florisboard.common.PointerMap
|
||||
@@ -114,6 +115,8 @@ fun TextKeyboardLayout(
|
||||
|
||||
val glideEnabled by prefs.glide.enabled.observeAsState()
|
||||
val glideShowTrail by prefs.glide.showTrail.observeAsState()
|
||||
val glideTrailColor = FlorisImeTheme.style.get(element = FlorisImeUi.GlideTrail)
|
||||
.foreground.solidColor(default = Color.Green)
|
||||
|
||||
val keyboard = renderInfo.keyboard
|
||||
val controller = remember { TextKeyboardLayoutController(context) }.also {
|
||||
@@ -155,7 +158,8 @@ fun TextKeyboardLayout(
|
||||
MotionEvent.ACTION_MOVE,
|
||||
MotionEvent.ACTION_POINTER_UP,
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
MotionEvent.ACTION_CANCEL
|
||||
-> {
|
||||
val clonedEvent = MotionEvent.obtain(event)
|
||||
touchEventChannel
|
||||
.trySend(clonedEvent)
|
||||
@@ -182,12 +186,14 @@ fun TextKeyboardLayout(
|
||||
controller.fadingGlide,
|
||||
targetDist,
|
||||
controller.fadingGlideRadius,
|
||||
radiusReductionFactor
|
||||
radiusReductionFactor,
|
||||
glideTrailColor,
|
||||
)
|
||||
}
|
||||
if (controller.isGliding && controller.glideDataForDrawing.isNotEmpty()) {
|
||||
controller.drawGlideTrail(
|
||||
this, controller.glideDataForDrawing, targetDist, radius, radiusReductionFactor
|
||||
this, controller.glideDataForDrawing, targetDist, radius,
|
||||
radiusReductionFactor, glideTrailColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -278,7 +284,7 @@ private fun TextKeyButton(
|
||||
isPressed = key.isPressed && key.isEnabled,
|
||||
isDisabled = !key.isEnabled,
|
||||
)
|
||||
val fontSize = keyStyle.fontSize.spSize() * fontSizeMultiplier * when (key.computedData.code) {
|
||||
val fontSize = keyStyle.fontSize.spSize() safeTimes fontSizeMultiplier safeTimes when (key.computedData.code) {
|
||||
KeyCode.VIEW_CHARACTERS,
|
||||
KeyCode.VIEW_SYMBOLS,
|
||||
KeyCode.VIEW_SYMBOLS2 -> 0.80f
|
||||
@@ -291,10 +297,18 @@ private fun TextKeyButton(
|
||||
.requiredSize(key.visibleBounds.size.toDpSize())
|
||||
.absoluteOffset { key.visibleBounds.topLeft.toIntOffset() },
|
||||
background = keyStyle.background,
|
||||
clip = true,
|
||||
elevation = keyStyle.shadowElevation,
|
||||
shape = keyStyle.shape,
|
||||
clip = true,
|
||||
) {
|
||||
key.label?.let { label ->
|
||||
if (key.computedData.code == KeyCode.SPACE) {
|
||||
val prefs by florisPreferenceModel()
|
||||
val displayLanguageName by prefs.keyboard.spaceBarLanguageDisplayEnabled.observeAsState()
|
||||
if (!displayLanguageName) {
|
||||
return@let
|
||||
}
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
@@ -317,7 +331,7 @@ private fun TextKeyButton(
|
||||
mode = renderInfo.state.inputMode.value,
|
||||
isPressed = key.isPressed,
|
||||
)
|
||||
val hintFontSize = keyHintStyle.fontSize.spSize() * fontSizeMultiplier
|
||||
val hintFontSize = keyHintStyle.fontSize.spSize() safeTimes fontSizeMultiplier
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(end = (key.visibleBounds.width / 12f).toDp())
|
||||
@@ -496,7 +510,10 @@ private class TextKeyboardLayoutController(
|
||||
pointer
|
||||
) || pointer.hasTriggeredGestureMove || pointer.shouldBlockNextUp
|
||||
) {
|
||||
if (pointer.hasTriggeredGestureMove && pointer.initialKey?.computedData?.code == KeyCode.DELETE) {
|
||||
if (pointer.hasTriggeredGestureMove &&
|
||||
pointer.initialKey?.computedData?.code == KeyCode.DELETE &&
|
||||
prefs.gestures.deleteKeySwipeLeft.get() != SwipeAction.SELECT_CHARACTERS_PRECISELY &&
|
||||
prefs.gestures.deleteKeySwipeLeft.get() != SwipeAction.SELECT_WORDS_PRECISELY) {
|
||||
activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
@@ -723,7 +740,7 @@ private class TextKeyboardLayoutController(
|
||||
|
||||
return when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (prefs.gestures.deleteKeySwipeLeft.get()) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY, SwipeAction.SELECT_CHARACTERS_PRECISELY -> {
|
||||
activeEditorInstance?.apply {
|
||||
if (abs(event.relUnitCountX) > 0) {
|
||||
inputFeedbackController?.gestureMovingSwipe(TextKeyData.DELETE)
|
||||
@@ -739,7 +756,7 @@ private class TextKeyboardLayoutController(
|
||||
pointer.shouldBlockNextUp = true
|
||||
true
|
||||
}
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> {
|
||||
SwipeAction.DELETE_WORDS_PRECISELY, SwipeAction.SELECT_WORDS_PRECISELY -> {
|
||||
activeEditorInstance?.apply {
|
||||
if (abs(event.relUnitCountX) > 0) {
|
||||
inputFeedbackController?.gestureMovingSwipe(TextKeyData.DELETE)
|
||||
@@ -881,6 +898,7 @@ private class TextKeyboardLayoutController(
|
||||
targetDist: Float,
|
||||
initialRadius: Float,
|
||||
radiusReductionFactor: Float,
|
||||
color: Color,
|
||||
) {
|
||||
var radius = initialRadius
|
||||
var drawnPoints = 0
|
||||
@@ -902,7 +920,7 @@ private class TextKeyboardLayoutController(
|
||||
gestureData[i].first.x * (1 - j.toFloat() / numPoints) + gestureData[i - 1].first.x * (j.toFloat() / numPoints)
|
||||
val intermediateY =
|
||||
gestureData[i].first.y * (1 - j.toFloat() / numPoints) + gestureData[i - 1].first.y * (j.toFloat() / numPoints)
|
||||
drawScope.drawCircle(Color.Green, radius, center = Offset(intermediateX, intermediateY))
|
||||
drawScope.drawCircle(color, radius, center = Offset(intermediateX, intermediateY))
|
||||
drawnPoints += 1
|
||||
prevX = intermediateX
|
||||
prevY = intermediateY
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -29,12 +28,10 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
@@ -53,6 +53,7 @@ import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggBackground
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggShadow
|
||||
import dev.patrickgold.florisboard.snygg.ui.solidColor
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
|
||||
@@ -140,6 +141,7 @@ private fun SmartbarPrimaryRow(modifier: Modifier = Modifier) = key(FlorisImeUi.
|
||||
.padding(4.dp)
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f)
|
||||
.snyggShadow(actionsToggleStyle)
|
||||
.snyggBackground(actionsToggleStyle.background, actionsToggleStyle.shape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@@ -201,6 +203,7 @@ private fun SmartbarPrimaryRow(modifier: Modifier = Modifier) = key(FlorisImeUi.
|
||||
.padding(4.dp)
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f)
|
||||
.snyggShadow(secondaryToggleStyle)
|
||||
.snyggBackground(secondaryToggleStyle.background, secondaryToggleStyle.shape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.keyboardManager
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggBackground
|
||||
import dev.patrickgold.florisboard.snygg.ui.snyggShadow
|
||||
import dev.patrickgold.florisboard.snygg.ui.solidColor
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
|
||||
@@ -91,6 +92,7 @@ fun SmartbarActionRow() = with(LocalDensity.current) {
|
||||
.padding(4.dp)
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f)
|
||||
.snyggShadow(moreStyle)
|
||||
.snyggBackground(moreStyle.background, moreStyle.shape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@@ -128,6 +130,7 @@ fun SmartbarActionRow() = with(LocalDensity.current) {
|
||||
.padding(4.dp)
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f)
|
||||
.snyggShadow(buttonStyle)
|
||||
.snyggBackground(buttonStyle.background, buttonStyle.shape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
|
||||
@@ -44,8 +44,6 @@ val FlorisImeThemeBaseStyle = SnyggStylesheet {
|
||||
"surface" to rgbaColor(66, 66, 66)
|
||||
"surfaceVariant" to rgbaColor(97, 97, 97)
|
||||
|
||||
"onPrimary" to rgbaColor(255, 255, 255)
|
||||
"onSecondary" to rgbaColor(255, 255, 255)
|
||||
"onBackground" to rgbaColor(255, 255, 255)
|
||||
"onSurface" to rgbaColor(255, 255, 255)
|
||||
}
|
||||
@@ -57,6 +55,7 @@ val FlorisImeThemeBaseStyle = SnyggStylesheet {
|
||||
background = `var`("surface")
|
||||
foreground = `var`("onSurface")
|
||||
fontSize = size(22.sp)
|
||||
shadowElevation = size(2.dp)
|
||||
shape = roundedCornerShape(20)
|
||||
}
|
||||
FlorisImeUi.Key(pressedSelector = true) {
|
||||
@@ -118,6 +117,10 @@ val FlorisImeThemeBaseStyle = SnyggStylesheet {
|
||||
shape = roundedCornerShape(12.dp)
|
||||
}
|
||||
|
||||
FlorisImeUi.GlideTrail {
|
||||
foreground = `var`("primary")
|
||||
}
|
||||
|
||||
FlorisImeUi.OneHandedPanel {
|
||||
background = rgbaColor(27, 94, 32)
|
||||
foreground = rgbaColor(238, 238, 238)
|
||||
@@ -129,12 +132,12 @@ val FlorisImeThemeBaseStyle = SnyggStylesheet {
|
||||
FlorisImeUi.SmartbarPrimaryActionRowToggle {
|
||||
background = `var`("surface")
|
||||
foreground = `var`("onSurface")
|
||||
shape = roundedCornerShape(50)
|
||||
shape = circleShape()
|
||||
}
|
||||
FlorisImeUi.SmartbarPrimarySecondaryRowToggle {
|
||||
background = rgbaColor(0, 0, 0, 0f)
|
||||
foreground = rgbaColor(144, 144, 144)
|
||||
shape = roundedCornerShape(50)
|
||||
shape = circleShape()
|
||||
}
|
||||
|
||||
FlorisImeUi.SmartbarSecondaryRow {
|
||||
@@ -147,7 +150,7 @@ val FlorisImeThemeBaseStyle = SnyggStylesheet {
|
||||
FlorisImeUi.SmartbarActionButton {
|
||||
background = rgbaColor(0, 0, 0, 0f)
|
||||
foreground = rgbaColor(220, 220, 220)
|
||||
shape = roundedCornerShape(50)
|
||||
shape = circleShape()
|
||||
}
|
||||
|
||||
FlorisImeUi.SmartbarCandidateRow {
|
||||
|
||||
@@ -26,6 +26,8 @@ object FlorisImeUi {
|
||||
const val ClipboardItem = "clipboard-item"
|
||||
const val ClipboardItemPopup = "clipboard-item-popup"
|
||||
|
||||
const val GlideTrail = "glide-trail"
|
||||
|
||||
const val OneHandedPanel = "one-handed-panel"
|
||||
|
||||
const val SmartbarPrimaryRow = "smartbar-primary-row"
|
||||
|
||||
@@ -20,11 +20,13 @@ import dev.patrickgold.florisboard.snygg.Snygg
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.snygg.SnyggPropertySetSpecBuilder
|
||||
import dev.patrickgold.florisboard.snygg.SnyggSpec
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerShapeDpValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerShapePercentValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCircleShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRectangleShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerShapeDpValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerShapePercentValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSpSizeValue
|
||||
|
||||
@@ -49,16 +51,24 @@ fun SnyggPropertySetSpecBuilder.fontSize() {
|
||||
supportedValues(SnyggSpSizeValue),
|
||||
)
|
||||
}
|
||||
fun SnyggPropertySetSpecBuilder.shadow() {
|
||||
property(
|
||||
name = Snygg.ShadowElevation,
|
||||
level = SnyggLevel.ADVANCED,
|
||||
supportedValues(SnyggDpSizeValue),
|
||||
)
|
||||
}
|
||||
fun SnyggPropertySetSpecBuilder.shape() {
|
||||
property(
|
||||
name = Snygg.Shape,
|
||||
level = SnyggLevel.ADVANCED,
|
||||
supportedValues(
|
||||
SnyggRectangleShapeValue,
|
||||
SnyggCutCornerShapeDpValue,
|
||||
SnyggCutCornerShapePercentValue,
|
||||
SnyggRoundedCornerShapeDpValue,
|
||||
SnyggRoundedCornerShapePercentValue,
|
||||
SnyggCircleShapeValue,
|
||||
SnyggRoundedCornerDpShapeValue,
|
||||
SnyggRoundedCornerPercentShapeValue,
|
||||
SnyggCutCornerDpShapeValue,
|
||||
SnyggCutCornerPercentShapeValue,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -71,6 +81,7 @@ object FlorisImeUiSpec : SnyggSpec({
|
||||
background()
|
||||
foreground()
|
||||
fontSize()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
element(FlorisImeUi.KeyHint) {
|
||||
@@ -83,6 +94,7 @@ object FlorisImeUiSpec : SnyggSpec({
|
||||
background()
|
||||
foreground()
|
||||
fontSize()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
|
||||
@@ -95,15 +107,21 @@ object FlorisImeUiSpec : SnyggSpec({
|
||||
background()
|
||||
foreground()
|
||||
fontSize()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
element(FlorisImeUi.ClipboardItemPopup) {
|
||||
background()
|
||||
foreground()
|
||||
fontSize()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
|
||||
element(FlorisImeUi.GlideTrail) {
|
||||
foreground()
|
||||
}
|
||||
|
||||
element(FlorisImeUi.OneHandedPanel) {
|
||||
background()
|
||||
foreground()
|
||||
@@ -115,11 +133,13 @@ object FlorisImeUiSpec : SnyggSpec({
|
||||
element(FlorisImeUi.SmartbarPrimaryActionRowToggle) {
|
||||
background()
|
||||
foreground()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
element(FlorisImeUi.SmartbarPrimarySecondaryRowToggle) {
|
||||
background()
|
||||
foreground()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
|
||||
@@ -133,6 +153,7 @@ object FlorisImeUiSpec : SnyggSpec({
|
||||
element(FlorisImeUi.SmartbarActionButton) {
|
||||
background()
|
||||
foreground()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
|
||||
@@ -159,6 +180,7 @@ object FlorisImeUiSpec : SnyggSpec({
|
||||
background()
|
||||
foreground()
|
||||
fontSize()
|
||||
shadow()
|
||||
shape()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,51 +17,43 @@
|
||||
package dev.patrickgold.florisboard.ime.theme
|
||||
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
private const val SERIAL_TYPE = "ime.extension.theme"
|
||||
|
||||
@SerialName(SERIAL_TYPE)
|
||||
@SerialName(ThemeExtension.SERIAL_TYPE)
|
||||
@Serializable
|
||||
data class ThemeExtension(
|
||||
class ThemeExtension(
|
||||
override val meta: ExtensionMeta,
|
||||
override val dependencies: List<String>? = null,
|
||||
val themes: List<ThemeExtensionComponent>,
|
||||
val themes: List<ThemeExtensionComponentImpl>,
|
||||
) : Extension() {
|
||||
|
||||
companion object {
|
||||
const val SERIAL_TYPE = "ime.extension.theme"
|
||||
}
|
||||
|
||||
override fun serialType() = SERIAL_TYPE
|
||||
|
||||
override fun components() = themes
|
||||
|
||||
override fun edit(): ExtensionEditor {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ThemeExtensionComponent(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val authors: List<String>,
|
||||
@SerialName("isNight")
|
||||
val isNightTheme: Boolean = true,
|
||||
val isBorderless: Boolean = false,
|
||||
val isMaterialYouAware: Boolean = false,
|
||||
@SerialName("stylesheet")
|
||||
val stylesheetPath: String? = null,
|
||||
) : ExtensionComponent {
|
||||
fun stylesheetPath() = "stylesheets/$id.json"
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun extCoreTheme(id: String): ExtensionComponentName {
|
||||
return ExtensionComponentName(
|
||||
extensionId = "org.florisboard.themes",
|
||||
componentId = id,
|
||||
override fun edit() = ThemeExtensionEditor(
|
||||
meta = meta,
|
||||
dependencies = dependencies?.toMutableList() ?: mutableListOf(),
|
||||
themes = themes.map { it.edit() }.toMutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
class ThemeExtensionEditor(
|
||||
override var meta: ExtensionMeta,
|
||||
override val dependencies: MutableList<String>,
|
||||
val themes: MutableList<ThemeExtensionComponentEditor>,
|
||||
) : ExtensionEditor {
|
||||
|
||||
override fun build() = ThemeExtension(
|
||||
meta = meta,
|
||||
dependencies = dependencies.takeUnless { it.isEmpty() }?.toList(),
|
||||
themes = themes.map { it.build() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.theme
|
||||
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.snygg.SnyggStylesheetEditor
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun extCoreTheme(id: String) = ExtensionComponentName(
|
||||
extensionId = "org.florisboard.themes",
|
||||
componentId = id,
|
||||
)
|
||||
|
||||
interface ThemeExtensionComponent : ExtensionComponent {
|
||||
companion object {
|
||||
fun defaultStylesheetPath(id: String): String {
|
||||
return "stylesheets/$id.json"
|
||||
}
|
||||
}
|
||||
|
||||
override val id: String
|
||||
override val label: String
|
||||
override val authors: List<String>
|
||||
val isNightTheme: Boolean
|
||||
val isBorderless: Boolean
|
||||
val isMaterialYouAware: Boolean
|
||||
val stylesheetPath: String?
|
||||
|
||||
fun stylesheetPath(): String = stylesheetPath.takeUnless { it.isNullOrBlank() } ?: defaultStylesheetPath(id)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ThemeExtensionComponentImpl(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val authors: List<String>,
|
||||
@SerialName("isNight")
|
||||
override val isNightTheme: Boolean = true,
|
||||
override val isBorderless: Boolean = false,
|
||||
override val isMaterialYouAware: Boolean = false,
|
||||
@SerialName("stylesheet")
|
||||
override val stylesheetPath: String? = null,
|
||||
) : ThemeExtensionComponent {
|
||||
|
||||
fun edit() = ThemeExtensionComponentEditor(
|
||||
id, label, authors, isNightTheme, isBorderless, isMaterialYouAware, stylesheetPath ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
class ThemeExtensionComponentEditor(
|
||||
override var id: String = "",
|
||||
override var label: String = "",
|
||||
override var authors: List<String> = emptyList(),
|
||||
override var isNightTheme: Boolean = true,
|
||||
override var isBorderless: Boolean = false,
|
||||
override var isMaterialYouAware: Boolean = false,
|
||||
override var stylesheetPath: String = "",
|
||||
) : ThemeExtensionComponent {
|
||||
|
||||
var stylesheetPathOnLoad: String? = null
|
||||
var stylesheetEditor: SnyggStylesheetEditor? = null
|
||||
|
||||
fun build(): ThemeExtensionComponentImpl {
|
||||
val component = ThemeExtensionComponentImpl(
|
||||
id = id.trim(),
|
||||
label = label.trim(),
|
||||
authors = authors.filterNot { it.isBlank() },
|
||||
isNightTheme = isNightTheme,
|
||||
isBorderless = isBorderless,
|
||||
isMaterialYouAware = isMaterialYouAware,
|
||||
stylesheetPath = stylesheetPath.takeUnless { it.isBlank() },
|
||||
)
|
||||
check(id.isNotBlank()) { "Theme component ID cannot be blank" }
|
||||
check(label.isNotBlank()) { "Theme component label cannot be blank" }
|
||||
check(authors.isNotEmpty()) { "Theme component authors must contain at least one non-blank author field" }
|
||||
return component
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,13 @@ package dev.patrickgold.florisboard.ime.theme
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.Icon
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.graphics.drawable.RippleDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
@@ -30,6 +35,7 @@ import androidx.autofill.inline.common.ImageViewStyle
|
||||
import androidx.autofill.inline.common.TextViewStyle
|
||||
import androidx.autofill.inline.common.ViewStyle
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
@@ -38,7 +44,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.appContext
|
||||
import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.common.ViewUtils
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.res.ZipUtils
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
@@ -53,9 +59,9 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import java.time.LocalTime
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
/**
|
||||
* Core class which manages the keyboard theme. Note, that this does not affect the UI theme of the
|
||||
* Settings Activities.
|
||||
@@ -72,92 +78,15 @@ class ThemeManager(context: Context) {
|
||||
var previewThemeId: ExtensionComponentName? by Delegates.observable(null) { _, _, _ ->
|
||||
updateActiveTheme()
|
||||
}
|
||||
var previewThemeInfo: ThemeInfo? by Delegates.observable(null) { _, _, _ ->
|
||||
updateActiveTheme()
|
||||
}
|
||||
|
||||
private val cachedThemeInfos = mutableListOf<ThemeInfo>()
|
||||
private val activeThemeGuard = Mutex(locked = false)
|
||||
private val _activeThemeInfo = MutableLiveData(ThemeInfo.DEFAULT)
|
||||
val activeThemeInfo: LiveData<ThemeInfo> get() = _activeThemeInfo
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a new inline suggestion UI bundle based on the attributes of the given [style].
|
||||
*
|
||||
* @param context The context of the parent view/controller.
|
||||
* @param style The theme from which the color attributes should be fetched. Defaults to
|
||||
* [FlorisImeThemeBaseStyle].
|
||||
*
|
||||
* @return A bundle containing all necessary attributes for the inline suggestion views to properly display.
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun createInlineSuggestionUiStyleBundle(context: Context, style: SnyggStylesheet = FlorisImeThemeBaseStyle): Bundle {
|
||||
val chipStyle = style.getStatic(FlorisImeUi.SmartbarPrimaryActionRowToggle)
|
||||
val bgColor = chipStyle.background.solidColor().toArgb()
|
||||
val fgColor = chipStyle.foreground.solidColor().toArgb()
|
||||
val bgDrawableId = R.drawable.chip_background
|
||||
val stylesBuilder = UiVersions.newStylesBuilder()
|
||||
val suggestionStyle = InlineSuggestionUi.newStyleBuilder()
|
||||
.setSingleIconChipStyle(
|
||||
ViewStyle.Builder()
|
||||
.setBackground(
|
||||
Icon.createWithResource(context, bgDrawableId).setTint(bgColor)
|
||||
)
|
||||
.setPadding(0, 0, 0, 0)
|
||||
.build()
|
||||
)
|
||||
.setChipStyle(
|
||||
ViewStyle.Builder()
|
||||
.setBackground(
|
||||
Icon.createWithResource(context, bgDrawableId).setTint(bgColor)
|
||||
)
|
||||
.setPadding(
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_start).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_top).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_end).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_bottom).toInt(),
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setStartIconStyle(
|
||||
ImageViewStyle.Builder()
|
||||
.setLayoutMargin(0, 0, 0, 0)
|
||||
.build()
|
||||
)
|
||||
.setTitleStyle(
|
||||
TextViewStyle.Builder()
|
||||
.setLayoutMargin(
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_start).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_top).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_end).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_bottom).toInt(),
|
||||
)
|
||||
.setTextColor(fgColor)
|
||||
.setTextSize(16f)
|
||||
.build()
|
||||
)
|
||||
.setSubtitleStyle(
|
||||
TextViewStyle.Builder()
|
||||
.setLayoutMargin(
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_start).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_top).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_end).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_bottom).toInt(),
|
||||
)
|
||||
.setTextColor(ColorUtils.setAlphaComponent(fgColor, 150))
|
||||
.setTextSize(14f)
|
||||
.build()
|
||||
)
|
||||
.setEndIconStyle(
|
||||
ImageViewStyle.Builder()
|
||||
.setLayoutMargin(0, 0, 0, 0)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
stylesBuilder.addStyle(suggestionStyle)
|
||||
return stylesBuilder.build()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
extensionManager.themes.observeForever { themeExtensions ->
|
||||
val map = buildMap {
|
||||
@@ -192,6 +121,10 @@ class ThemeManager(context: Context) {
|
||||
private fun updateActiveTheme(action: () -> Unit = { }) = scope.launch {
|
||||
activeThemeGuard.withLock {
|
||||
action()
|
||||
previewThemeInfo?.let { previewThemeInfo ->
|
||||
_activeThemeInfo.postValue(previewThemeInfo)
|
||||
return@withLock
|
||||
}
|
||||
val activeName = evaluateActiveThemeName()
|
||||
val cachedInfo = cachedThemeInfos.find { it.name == activeName }
|
||||
if (cachedInfo != null) {
|
||||
@@ -235,24 +168,105 @@ class ThemeManager(context: Context) {
|
||||
prefs.theme.dayThemeId.get()
|
||||
}
|
||||
ThemeMode.FOLLOW_TIME -> {
|
||||
if (AndroidVersion.ATLEAST_API26_O) {
|
||||
val current = LocalTime.now()
|
||||
val sunrise = prefs.theme.sunriseTime.get()
|
||||
val sunset = prefs.theme.sunsetTime.get()
|
||||
if (current in sunrise..sunset) {
|
||||
prefs.theme.dayThemeId.get()
|
||||
} else {
|
||||
prefs.theme.nightThemeId.get()
|
||||
}
|
||||
} else {
|
||||
//if (AndroidVersion.ATLEAST_API26_O) {
|
||||
// val current = LocalTime.now()
|
||||
// val sunrise = prefs.theme.sunriseTime.get()
|
||||
// val sunset = prefs.theme.sunsetTime.get()
|
||||
// if (current in sunrise..sunset) {
|
||||
// prefs.theme.dayThemeId.get()
|
||||
// } else {
|
||||
// prefs.theme.nightThemeId.get()
|
||||
// }
|
||||
//} else {
|
||||
prefs.theme.nightThemeId.get()
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new inline suggestion UI bundle based on the attributes of the given [style].
|
||||
*
|
||||
* @param context The context of the parent view/controller.
|
||||
* @param style The theme from which the color attributes should be fetched. Defaults to
|
||||
* [FlorisImeThemeBaseStyle].
|
||||
*
|
||||
* @return A bundle containing all necessary attributes for the inline suggestion views to properly display.
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun createInlineSuggestionUiStyleBundle(
|
||||
context: Context,
|
||||
style: SnyggStylesheet = activeThemeInfo.value?.stylesheet ?: FlorisImeThemeBaseStyle,
|
||||
): Bundle {
|
||||
val chipStyle = style.getStatic(FlorisImeUi.SmartbarPrimaryActionRowToggle)
|
||||
val bgColor = chipStyle.background.solidColor()
|
||||
val fgColor = chipStyle.foreground.solidColor()
|
||||
val bgDrawableId = R.drawable.autofill_inline_suggestion_chip_background
|
||||
val stylesBuilder = UiVersions.newStylesBuilder()
|
||||
val suggestionStyle = InlineSuggestionUi.newStyleBuilder()
|
||||
.setSingleIconChipStyle(
|
||||
ViewStyle.Builder()
|
||||
.setBackground(
|
||||
Icon.createWithResource(context, bgDrawableId).setTint(bgColor.toArgb())
|
||||
)
|
||||
.setPadding(0, 0, 0, 0)
|
||||
.build()
|
||||
)
|
||||
.setChipStyle(
|
||||
ViewStyle.Builder()
|
||||
.setBackground(
|
||||
Icon.createWithResource(context, bgDrawableId).setTint(bgColor.toArgb())
|
||||
)
|
||||
.setPadding(
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_start).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_top).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_end).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_bottom).toInt(),
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setStartIconStyle(
|
||||
ImageViewStyle.Builder()
|
||||
.setLayoutMargin(0, 0, 0, 0)
|
||||
.build()
|
||||
)
|
||||
.setTitleStyle(
|
||||
TextViewStyle.Builder()
|
||||
.setLayoutMargin(
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_start).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_top).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_end).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_bottom).toInt(),
|
||||
)
|
||||
.setTextColor(fgColor.toArgb())
|
||||
.setTextSize(16f)
|
||||
.build()
|
||||
)
|
||||
.setSubtitleStyle(
|
||||
TextViewStyle.Builder()
|
||||
.setLayoutMargin(
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_start).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_top).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_end).toInt(),
|
||||
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_bottom).toInt(),
|
||||
)
|
||||
.setTextColor(ColorUtils.setAlphaComponent(fgColor.toArgb(), 150))
|
||||
.setTextSize(14f)
|
||||
.build()
|
||||
)
|
||||
.setEndIconStyle(
|
||||
ImageViewStyle.Builder()
|
||||
.setLayoutMargin(0, 0, 0, 0)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
stylesBuilder.addStyle(suggestionStyle)
|
||||
return stylesBuilder.build()
|
||||
}
|
||||
|
||||
private fun getColorFromThemeAttribute(
|
||||
context: Context, typedValue: TypedValue, @AttrRes attr: Int
|
||||
context: Context, typedValue: TypedValue, @AttrRes attr: Int,
|
||||
): Int? {
|
||||
return if (context.theme.resolveAttribute(attr, typedValue, true)) {
|
||||
if (typedValue.type == TypedValue.TYPE_REFERENCE) {
|
||||
@@ -273,7 +287,7 @@ class ThemeManager(context: Context) {
|
||||
companion object {
|
||||
val DEFAULT = ThemeInfo(
|
||||
name = extCoreTheme("base"),
|
||||
config = ThemeExtensionComponent(id = "base", label = "Base", authors = listOf()),
|
||||
config = ThemeExtensionComponentImpl(id = "base", label = "Base", authors = listOf()),
|
||||
stylesheet = FlorisImeThemeBaseStyle.compileToFullyQualified(FlorisImeUiSpec),
|
||||
)
|
||||
}
|
||||
@@ -289,4 +303,48 @@ class ThemeManager(context: Context) {
|
||||
val DEFAULT = RemoteColors("undefined", null, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun autofillChipBackgroundOf(
|
||||
bgColor: Color,
|
||||
rippleColor: Color,
|
||||
): Drawable {
|
||||
val cornerRadius = ViewUtils.dp2px(32f)
|
||||
val shadowColors = intArrayOf(
|
||||
Color.Transparent.toArgb(),
|
||||
Color.Transparent.toArgb(),
|
||||
Color(red = 0x00, green = 0x00, blue = 0x00, alpha = 0x1F).toArgb(),
|
||||
)
|
||||
val shadowDistribution = floatArrayOf(0f, 0.5f, 1f)
|
||||
val padding = ViewUtils.dp2px(5f).toInt()
|
||||
|
||||
fun gradientDrawableOf() = GradientDrawable().also {
|
||||
it.shape = GradientDrawable.RECTANGLE
|
||||
it.cornerRadius = cornerRadius
|
||||
it.setGradientCenter(0.5f, 0.5f)
|
||||
it.gradientType = GradientDrawable.LINEAR_GRADIENT
|
||||
it.setColors(shadowColors, shadowDistribution)
|
||||
}
|
||||
|
||||
val layerList = LayerDrawable(arrayOf(
|
||||
gradientDrawableOf().also {
|
||||
it.orientation = GradientDrawable.Orientation.BOTTOM_TOP
|
||||
},
|
||||
gradientDrawableOf().also {
|
||||
it.orientation = GradientDrawable.Orientation.LEFT_RIGHT
|
||||
},
|
||||
gradientDrawableOf().also {
|
||||
it.orientation = GradientDrawable.Orientation.TOP_BOTTOM
|
||||
},
|
||||
gradientDrawableOf().also {
|
||||
it.orientation = GradientDrawable.Orientation.RIGHT_LEFT
|
||||
},
|
||||
GradientDrawable().also {
|
||||
it.shape = GradientDrawable.RECTANGLE
|
||||
it.cornerRadius = cornerRadius
|
||||
it.setColor(bgColor.toArgb())
|
||||
},
|
||||
)).also { it.setLayerInset(4, padding, padding, padding, padding) }
|
||||
return RippleDrawable(ColorStateList.valueOf(rippleColor.toArgb()), layerList, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@ import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.io.FsFile
|
||||
|
||||
object FileRegistry {
|
||||
val BackupArchive = Entry(
|
||||
type = Type.BINARY,
|
||||
fileExt = "zip",
|
||||
mediaType = "application/zip",
|
||||
alternativeMediaTypes = listOf(
|
||||
"application/octet-stream",
|
||||
),
|
||||
)
|
||||
|
||||
val FlexExtension = Entry(
|
||||
type = Type.BINARY,
|
||||
fileExt = "flex",
|
||||
|
||||
@@ -70,6 +70,17 @@ object ZipUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun zip(srcDir: FsDir, dstFile: FsFile) {
|
||||
check(srcDir.exists() && srcDir.isDirectory) { "Cannot zip standalone file." }
|
||||
dstFile.parentFile?.mkdirs()
|
||||
dstFile.delete()
|
||||
FileOutputStream(dstFile).use { outStream ->
|
||||
ZipOutputStream(outStream).use { zipOut ->
|
||||
zip(srcDir, zipOut, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun zip(context: Context, srcDir: FsDir, uri: Uri) = runCatching {
|
||||
check(srcDir.exists() && srcDir.isDirectory) { "Cannot zip standalone file." }
|
||||
context.contentResolver.write(uri) { fileOut ->
|
||||
|
||||
@@ -19,14 +19,22 @@ package dev.patrickgold.florisboard.res.cache
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.neverEqualPolicy
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.patrickgold.florisboard.app.ui.ext.EditorAction
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.Backup
|
||||
import dev.patrickgold.florisboard.appContext
|
||||
import dev.patrickgold.florisboard.common.android.query
|
||||
import dev.patrickgold.florisboard.common.android.readToFile
|
||||
import dev.patrickgold.florisboard.ime.nlp.NATIVE_NULLPTR
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.FileRegistry
|
||||
import dev.patrickgold.florisboard.res.ZipUtils
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionDefaults
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionJsonConfig
|
||||
import dev.patrickgold.florisboard.res.io.FsDir
|
||||
import dev.patrickgold.florisboard.res.io.FsFile
|
||||
@@ -47,21 +55,22 @@ import java.util.*
|
||||
|
||||
class CacheManager(context: Context) {
|
||||
companion object {
|
||||
private const val InputDirName = "input"
|
||||
private const val OutputDirName = "output"
|
||||
|
||||
private const val ImporterDirName = "importer"
|
||||
private const val ImporterInputDirName = "input"
|
||||
private const val ImporterOutputDirName = "output"
|
||||
|
||||
private const val ExporterDirName = "exporter"
|
||||
|
||||
private const val EditorDirName = "editor"
|
||||
private const val BackupAndRestoreDirName = "backup-and-restore"
|
||||
}
|
||||
|
||||
private val appContext by context.appContext()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
val importer = WorkspacesContainer<ImporterWorkspace>(ImporterDirName)
|
||||
val exporter = WorkspacesContainer<ExporterWorkspace>(ExporterDirName)
|
||||
val editor = WorkspacesContainer<EditorWorkspace>(EditorDirName)
|
||||
val importer = WorkspacesContainer(ImporterDirName) { ImporterWorkspace(it) }
|
||||
val exporter = WorkspacesContainer(ExporterDirName) { ExporterWorkspace(it) }
|
||||
val themeExtEditor = WorkspacesContainer(EditorDirName) { ExtEditorWorkspace<ThemeExtensionEditor>(it) }
|
||||
val backupAndRestore = WorkspacesContainer(BackupAndRestoreDirName) { BackupAndRestoreWorkspace(it) }
|
||||
|
||||
fun readFromUriIntoCache(uri: Uri) = readFromUriIntoCache(listOf(uri))
|
||||
|
||||
@@ -96,12 +105,19 @@ class CacheManager(context: Context) {
|
||||
return workspace
|
||||
}
|
||||
|
||||
inner class WorkspacesContainer<T : Workspace> internal constructor(val dirName: String) {
|
||||
open inner class WorkspacesContainer<T : Workspace> internal constructor(
|
||||
val dirName: String,
|
||||
val factory: (uuid: String) -> T,
|
||||
) {
|
||||
private val workspacesGuard = Mutex(locked = false)
|
||||
private val workspaces = mutableListOf<T>()
|
||||
|
||||
val dir: FsDir = appContext.cacheDir.subDir(dirName)
|
||||
|
||||
fun new(uuid: String = UUID.randomUUID().toString()): T {
|
||||
return factory(uuid).also { it.mkdirs(); add(it) }
|
||||
}
|
||||
|
||||
internal fun add(workspace: T) = scope.launch {
|
||||
workspacesGuard.withLock {
|
||||
workspaces.add(workspace)
|
||||
@@ -130,6 +146,10 @@ class CacheManager(context: Context) {
|
||||
dir.mkdirs()
|
||||
}
|
||||
|
||||
fun isOpen() = dir.exists()
|
||||
|
||||
fun isClosed() = !dir.exists()
|
||||
|
||||
override fun close() {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
@@ -138,8 +158,8 @@ class CacheManager(context: Context) {
|
||||
inner class ImporterWorkspace(uuid: String) : Workspace(uuid) {
|
||||
override val dir: FsDir = importer.dir.subDir(uuid)
|
||||
|
||||
val inputDir: FsDir = dir.subDir(ImporterInputDirName)
|
||||
val outputDir: FsDir = dir.subDir(ImporterOutputDirName)
|
||||
val inputDir: FsDir = dir.subDir(InputDirName)
|
||||
val outputDir: FsDir = dir.subDir(OutputDirName)
|
||||
|
||||
var inputFileInfos = emptyList<FileInfo>()
|
||||
|
||||
@@ -159,8 +179,55 @@ class CacheManager(context: Context) {
|
||||
override val dir: FsDir = exporter.dir.subDir(uuid)
|
||||
}
|
||||
|
||||
inner class EditorWorkspace(uuid: String) : Workspace(uuid) {
|
||||
override val dir: FsDir = editor.dir.subDir(uuid)
|
||||
inner class ExtEditorWorkspace<T : ExtensionEditor>(uuid: String) : Workspace(uuid) {
|
||||
override val dir: FsDir = themeExtEditor.dir.subDir(uuid)
|
||||
|
||||
val extDir: FsDir = dir.subDir("ext")
|
||||
val saverDir: FsDir = dir.subDir("saver")
|
||||
|
||||
var currentAction by mutableStateOf<EditorAction?>(null)
|
||||
var ext: Extension? = null
|
||||
var editor by mutableStateOf<T?>(null, neverEqualPolicy())
|
||||
var version by mutableStateOf(0)
|
||||
|
||||
val isModified get() = version > 0
|
||||
|
||||
override fun mkdirs() {
|
||||
super.mkdirs()
|
||||
extDir.mkdirs()
|
||||
saverDir.mkdirs()
|
||||
}
|
||||
|
||||
inline fun <R> update(block: T.() -> R): R {
|
||||
// Method is designed to only be called when editor has been previously initialized
|
||||
val ret = block(editor!!)
|
||||
editor = editor
|
||||
version++
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
inner class BackupAndRestoreWorkspace(uuid: String) : Workspace(uuid) {
|
||||
override val dir: FsDir = backupAndRestore.dir.subDir(uuid)
|
||||
|
||||
val inputDir: FsDir = dir.subDir(InputDirName)
|
||||
val outputDir: FsDir = dir.subDir(OutputDirName)
|
||||
|
||||
lateinit var zipFile: FsFile
|
||||
lateinit var metadata: Backup.Metadata
|
||||
var restoreWarningId: Int? = null
|
||||
var restoreErrorId: Int? = null
|
||||
|
||||
override fun mkdirs() {
|
||||
super.mkdirs()
|
||||
inputDir.mkdirs()
|
||||
outputDir.mkdirs()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
super.close()
|
||||
backupAndRestore.remove(this)
|
||||
}
|
||||
}
|
||||
|
||||
data class FileInfo(
|
||||
|
||||
@@ -114,6 +114,8 @@ abstract class Extension {
|
||||
}
|
||||
|
||||
interface ExtensionEditor {
|
||||
val meta: ExtensionMetaEditor
|
||||
val workingDir: FsDir?
|
||||
var meta: ExtensionMeta
|
||||
val dependencies: MutableList<String>
|
||||
|
||||
fun build(): Extension
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package dev.patrickgold.florisboard.res.ext
|
||||
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import dev.patrickgold.florisboard.common.kotlin.tryOrNull
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -48,11 +50,16 @@ data class ExtensionComponentName(
|
||||
companion object {
|
||||
private const val DELIMITER = ":"
|
||||
|
||||
fun from(str: String) = runCatching<ExtensionComponentName> {
|
||||
fun from(str: String): ExtensionComponentName {
|
||||
val data = str.split(DELIMITER)
|
||||
check(data.size == 2) { "Extension component name must be of format <ext_id>:<comp_id>" }
|
||||
ExtensionComponentName(data[0], data[1])
|
||||
return ExtensionComponentName(data[0], data[1])
|
||||
}
|
||||
|
||||
val Saver = Saver<ExtensionComponentName?, String>(
|
||||
save = { it.toString() },
|
||||
restore = { tryOrNull { from(it) } },
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
@@ -67,15 +74,15 @@ data class ExtensionComponentName(
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ExtensionComponentName) {
|
||||
encoder.encodeString("${value.extensionId}:${value.componentId}")
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(value: String): ExtensionComponentName? {
|
||||
return from(value).getOrNull()
|
||||
return tryOrNull { from(value) }
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): ExtensionComponentName {
|
||||
return from(decoder.decodeString()).getOrThrow()
|
||||
return from(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,6 @@ data class ExtensionMaintainer(
|
||||
}
|
||||
}
|
||||
|
||||
fun edit() = ExtensionMaintainerEditor(name, email ?: "", url ?: "")
|
||||
|
||||
override fun toString() = buildString {
|
||||
append(name)
|
||||
if (email != null && email.isNotBlank()) {
|
||||
@@ -71,22 +69,6 @@ data class ExtensionMaintainer(
|
||||
}
|
||||
}
|
||||
|
||||
data class ExtensionMaintainerEditor(
|
||||
var name: String = "",
|
||||
var email: String = "",
|
||||
var url: String = "",
|
||||
) {
|
||||
fun build() = runCatching {
|
||||
val maintainer = ExtensionMaintainer(
|
||||
name.trim(),
|
||||
email.trim().ifBlank { null },
|
||||
url.trim().ifBlank { null },
|
||||
)
|
||||
check(maintainer.name.isNotBlank()) { "Extension maintainer name cannot be blank" }
|
||||
return@runCatching maintainer
|
||||
}
|
||||
}
|
||||
|
||||
private class ExtensionMaintainerSerializer : KSerializer<ExtensionMaintainer> {
|
||||
override val descriptor = PrimitiveSerialDescriptor("ExtensionMaintainer", PrimitiveKind.STRING)
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ val ExtensionJsonConfig = Json {
|
||||
|
||||
class ExtensionManager(context: Context) {
|
||||
companion object {
|
||||
private const val IME_KEYBOARD_PATH = "ime/keyboard"
|
||||
private const val IME_SPELLING_PATH = "ime/spelling"
|
||||
private const val IME_THEME_PATH = "ime/theme"
|
||||
const val IME_KEYBOARD_PATH = "ime/keyboard"
|
||||
const val IME_SPELLING_PATH = "ime/spelling"
|
||||
const val IME_THEME_PATH = "ime/theme"
|
||||
}
|
||||
|
||||
private val appContext by context.appContext()
|
||||
@@ -155,6 +155,7 @@ class ExtensionManager(context: Context) {
|
||||
}
|
||||
|
||||
init {
|
||||
value = emptyList()
|
||||
ioScope.launch {
|
||||
internalModuleDir.mkdirs()
|
||||
staticExtensions = indexAssetsModule()
|
||||
|
||||
@@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
/**
|
||||
* Interface for an `extension.json` file, which serves as a configuration of an extension
|
||||
* Class for an `extension.json` file, which serves as a configuration of an extension
|
||||
* package for FlorisBoard (`.flex` archive files).
|
||||
*
|
||||
* Files which are always read (case sensitive):
|
||||
@@ -39,7 +39,8 @@ import kotlinx.serialization.json.JsonNames
|
||||
data class ExtensionMeta(
|
||||
/**
|
||||
* The unique identifier of this extension, adhering to
|
||||
* [Java™ package name standards](https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html).
|
||||
* [Java™ package name standards](https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html)
|
||||
* and this regex: `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`
|
||||
*/
|
||||
val id: String,
|
||||
|
||||
@@ -100,41 +101,4 @@ data class ExtensionMeta(
|
||||
* Use an SPDX license expression if this extension has multiple licenses.
|
||||
*/
|
||||
val license: String,
|
||||
) {
|
||||
fun edit() = ExtensionMetaEditor(
|
||||
id, version, title, description ?: "", keywords?.toMutableList() ?: mutableListOf(),
|
||||
homepage ?: "", issueTracker ?: "", maintainers.map { it.edit() }.toMutableList(), license
|
||||
)
|
||||
}
|
||||
|
||||
data class ExtensionMetaEditor(
|
||||
var id: String = "",
|
||||
var version: String = "",
|
||||
var title: String = "",
|
||||
var description: String = "",
|
||||
var keywords: MutableList<String> = mutableListOf(),
|
||||
var homepage: String = "",
|
||||
var issueTracker: String = "",
|
||||
var maintainers: MutableList<ExtensionMaintainerEditor> = mutableListOf(),
|
||||
var license: String = "",
|
||||
) {
|
||||
fun build() = runCatching {
|
||||
val meta = ExtensionMeta(
|
||||
id.trim(),
|
||||
version.trim(),
|
||||
title.trim(),
|
||||
description.trim().ifBlank { null },
|
||||
keywords.mapNotNull { it.trim().ifBlank { null } }.ifEmpty { null },
|
||||
homepage.trim().ifBlank { null },
|
||||
issueTracker.trim().ifBlank { null },
|
||||
maintainers.map { it.build().getOrThrow() },
|
||||
license.trim(),
|
||||
)
|
||||
check(meta.id.isNotBlank()) { "Extension ID cannot be blank" }
|
||||
check(meta.version.isNotBlank()) { "Extension version string cannot be blank" }
|
||||
check(meta.title.isNotBlank()) { "Extension title cannot be blank" }
|
||||
check(meta.maintainers.isNotEmpty()) { "At least one extension maintainer must be defined" }
|
||||
check(meta.license.isNotBlank()) { "Extension license identifier cannot be blank" }
|
||||
return@runCatching meta
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.res.ext
|
||||
|
||||
import androidx.core.text.trimmedLength
|
||||
import dev.patrickgold.florisboard.common.ValidationRule
|
||||
import dev.patrickgold.florisboard.common.validate
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.snygg.SnyggStylesheet
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue
|
||||
|
||||
// TODO: (priority=medium)
|
||||
// make all strings available for localize
|
||||
object ExtensionValidation {
|
||||
private val MetaIdRegex = """^[a-z][a-z0-9_]*(\.[a-z0-9][a-z0-9_]*)*${'$'}""".toRegex()
|
||||
private val ComponentIdRegex = """^[a-z][a-z0-9_]*${'$'}""".toRegex()
|
||||
private val ThemeComponentStylesheetPathRegex = """^[^:*<>"']*${'$'}""".toRegex()
|
||||
|
||||
val MetaId = ValidationRule<String> {
|
||||
forKlass = ExtensionMeta::class
|
||||
forProperty = "id"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a package name")
|
||||
MetaIdRegex.matches(str) -> resultValid()
|
||||
else -> resultInvalid("Package name does not match regex $MetaIdRegex")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val MetaVersion = ValidationRule<String> {
|
||||
forKlass = ExtensionMeta::class
|
||||
forProperty = "version"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a version")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val MetaTitle = ValidationRule<String> {
|
||||
forKlass = ExtensionMeta::class
|
||||
forProperty = "title"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a title")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val MetaMaintainers = ValidationRule<String> {
|
||||
forKlass = ExtensionMeta::class
|
||||
forProperty = "maintainers"
|
||||
validator { str ->
|
||||
val maintainers = str.lines().filter { it.isNotBlank() }
|
||||
when {
|
||||
maintainers.isEmpty() -> resultInvalid(error = "Please enter at least one valid maintainer")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val MetaLicense = ValidationRule<String> {
|
||||
forKlass = ExtensionMeta::class
|
||||
forProperty = "license"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a license identifier")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ComponentId = ValidationRule<String> {
|
||||
forKlass = ExtensionComponent::class
|
||||
forProperty = "id"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a component ID")
|
||||
!ComponentIdRegex.matches(str) -> resultInvalid(error = "Please enter a component ID matching $ComponentIdRegex")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ComponentLabel = ValidationRule<String> {
|
||||
forKlass = ExtensionComponent::class
|
||||
forProperty = "label"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a component label")
|
||||
str.trimmedLength() > 30 -> resultValid(hint = "Your component label is quite long, which may lead to clipping in the UI")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ComponentAuthors = ValidationRule<String> {
|
||||
forKlass = ExtensionComponent::class
|
||||
forProperty = "authors"
|
||||
validator { str ->
|
||||
val authors = str.lines().filter { it.isNotBlank() }
|
||||
when {
|
||||
authors.isEmpty() -> resultInvalid(error = "Please enter at least one valid author")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ThemeComponentStylesheetPath = ValidationRule<String> {
|
||||
forKlass = ThemeExtensionComponent::class
|
||||
forProperty = "stylesheetPath"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isEmpty() -> resultValid()
|
||||
str.isBlank() -> resultInvalid(error = "The stylesheet path must not be blank")
|
||||
!ThemeComponentStylesheetPathRegex.matches(str) -> {
|
||||
resultInvalid(error = "Please enter a valid stylesheet path matching $ThemeComponentStylesheetPathRegex")
|
||||
}
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ThemeComponentVariableName = ValidationRule<String> {
|
||||
forKlass = SnyggStylesheet::class
|
||||
forProperty = "propertyName"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a variable name")
|
||||
str == "-" || str.startsWith("--") -> resultValid()
|
||||
else -> resultValid(hint = "By convention a FlorisCSS variable name starts with two dashes (--)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SnyggSolidColorValue = ValidationRule<String> {
|
||||
forKlass = SnyggSolidColorValue::class
|
||||
forProperty = "color"
|
||||
validator { input ->
|
||||
val str = input.trim()
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a color string")
|
||||
dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue.deserialize(str).isFailure -> {
|
||||
resultInvalid(error = "Please enter a valid color string")
|
||||
}
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SnyggDpShapeValue = ValidationRule<String> {
|
||||
forKlass = SnyggDpShapeValue::class
|
||||
forProperty = "corner"
|
||||
validator { str ->
|
||||
val floatValue = str.toFloatOrNull()
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a dp size")
|
||||
floatValue == null -> resultInvalid(error = "Please enter a valid number")
|
||||
floatValue < 0f -> resultInvalid(error = "Please enter a positive number (>=0)")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SnyggPercentShapeValue = ValidationRule<String> {
|
||||
forKlass = SnyggPercentShapeValue::class
|
||||
forProperty = "corner"
|
||||
validator { str ->
|
||||
val intValue = str.toIntOrNull()
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a percent size")
|
||||
intValue == null -> resultInvalid(error = "Please enter a valid number")
|
||||
intValue < 0 -> resultInvalid(error = "Please enter a positive number (>=0)")
|
||||
intValue > 50 -> resultValid(hint = "Any value above 50% will behave as if you set 50%, consider lowering your percent size")
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ExtensionMeta.validate(): Boolean {
|
||||
return with(ExtensionValidation) {
|
||||
validate(MetaId, id).isValid() &&
|
||||
validate(MetaVersion, version).isValid() &&
|
||||
validate(MetaTitle, title).isValid() &&
|
||||
validate(MetaMaintainers, maintainers.joinToString("\n")).isValid() &&
|
||||
validate(MetaLicense, license).isValid()
|
||||
}
|
||||
}
|
||||
@@ -54,3 +54,5 @@ inline fun <reified T> FsFile.writeJson(value: T, config: StringFormat = Json) {
|
||||
val text = config.encodeToString(value)
|
||||
return this.writeText(text)
|
||||
}
|
||||
|
||||
inline val FsFile/* and FsDir */.parentDir: FsDir? get() = this.parentFile
|
||||
|
||||
@@ -26,6 +26,7 @@ object Snygg {
|
||||
const val Height = "height"
|
||||
|
||||
const val Background = "background"
|
||||
const val Foreground = "foreground"
|
||||
|
||||
const val Border = "border"
|
||||
const val BorderTop = "border-top"
|
||||
@@ -39,9 +40,7 @@ object Snygg {
|
||||
const val FontVariant = "font-variant"
|
||||
const val FontWeight = "font-weight"
|
||||
|
||||
const val Foreground = "foreground"
|
||||
|
||||
const val Shadow = "shadow"
|
||||
const val ShadowElevation = "shadow-elevation"
|
||||
|
||||
const val Shape = "shape"
|
||||
}
|
||||
|
||||
@@ -16,14 +16,45 @@
|
||||
|
||||
package dev.patrickgold.florisboard.snygg
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
|
||||
|
||||
/**
|
||||
* SnyggLevel indicates if a rule property is intended to be edited by all users (BASIC) or only by advanced users
|
||||
* (ADVANCED). This level is intended for theme editor UIs to hide certain properties in a "basic" mode, for the Snygg
|
||||
* theme engine internally this level will be ignored completely.
|
||||
*/
|
||||
enum class SnyggLevel {
|
||||
enum class SnyggLevel : Comparable<SnyggLevel> {
|
||||
/** A property is intended to be edited by all users **/
|
||||
BASIC,
|
||||
/** A property is intended to be edited by advanced users **/
|
||||
ADVANCED;
|
||||
ADVANCED,
|
||||
/** A property is intended to be edited by developers **/
|
||||
DEVELOPER;
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun listEntries() = listPrefEntries {
|
||||
entry(
|
||||
key = BASIC,
|
||||
label = stringRes(R.string.enum__snygg_level__basic),
|
||||
description = stringRes(R.string.enum__snygg_level__basic__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
entry(
|
||||
key = ADVANCED,
|
||||
label = stringRes(R.string.enum__snygg_level__advanced),
|
||||
description = stringRes(R.string.enum__snygg_level__advanced__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
entry(
|
||||
key = DEVELOPER,
|
||||
label = stringRes(R.string.enum__snygg_level__developer),
|
||||
description = stringRes(R.string.enum__snygg_level__developer__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,23 +18,21 @@ package dev.patrickgold.florisboard.snygg
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import dev.patrickgold.florisboard.snygg.value.RgbaColor
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerShapeDpValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerShapePercentValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCircleShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggCutCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDefinedVarValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggImageRefValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggImplicitInheritValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggPercentageSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRectangleShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerShapeDpValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerShapePercentValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerDpShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggRoundedCornerPercentShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValue
|
||||
@@ -44,6 +42,7 @@ class SnyggPropertySet(val properties: Map<String, SnyggValue>) {
|
||||
val height = properties[Snygg.Height] ?: SnyggImplicitInheritValue
|
||||
|
||||
val background = properties[Snygg.Background] ?: SnyggImplicitInheritValue
|
||||
val foreground = properties[Snygg.Foreground] ?: SnyggImplicitInheritValue
|
||||
|
||||
val borderTop = properties[Snygg.BorderTop] ?: properties[Snygg.Border] ?: SnyggImplicitInheritValue
|
||||
val borderBottom = properties[Snygg.BorderBottom] ?: properties[Snygg.Border] ?: SnyggImplicitInheritValue
|
||||
@@ -56,9 +55,7 @@ class SnyggPropertySet(val properties: Map<String, SnyggValue>) {
|
||||
val fontVariant = properties[Snygg.FontVariant] ?: SnyggImplicitInheritValue
|
||||
val fontWeight = properties[Snygg.FontWeight] ?: SnyggImplicitInheritValue
|
||||
|
||||
val foreground = properties[Snygg.Foreground] ?: SnyggImplicitInheritValue
|
||||
|
||||
val shadow = properties[Snygg.Shadow] ?: SnyggImplicitInheritValue
|
||||
val shadowElevation = properties[Snygg.ShadowElevation] ?: SnyggImplicitInheritValue
|
||||
|
||||
val shape = properties[Snygg.Shape] ?: SnyggImplicitInheritValue
|
||||
|
||||
@@ -107,6 +104,9 @@ class SnyggPropertySetEditor(initProperties: Map<String, SnyggValue>? = null) {
|
||||
var background: SnyggValue?
|
||||
get() = getProperty(Snygg.Background)
|
||||
set(v) = setProperty(Snygg.Background, v)
|
||||
var foreground: SnyggValue?
|
||||
get() = getProperty(Snygg.Foreground)
|
||||
set(v) = setProperty(Snygg.Foreground, v)
|
||||
|
||||
var borderTop: SnyggValue?
|
||||
get() = getProperty(Snygg.BorderTop)
|
||||
@@ -137,13 +137,9 @@ class SnyggPropertySetEditor(initProperties: Map<String, SnyggValue>? = null) {
|
||||
get() = getProperty(Snygg.FontWeight)
|
||||
set(v) = setProperty(Snygg.FontWeight, v)
|
||||
|
||||
var foreground: SnyggValue?
|
||||
get() = getProperty(Snygg.Foreground)
|
||||
set(v) = setProperty(Snygg.Foreground, v)
|
||||
|
||||
var shadow: SnyggValue?
|
||||
get() = getProperty(Snygg.Shadow)
|
||||
set(v) = setProperty(Snygg.Shadow, v)
|
||||
var shadowElevation: SnyggValue?
|
||||
get() = getProperty(Snygg.ShadowElevation)
|
||||
set(v) = setProperty(Snygg.ShadowElevation, v)
|
||||
|
||||
var shape: SnyggValue?
|
||||
get() = getProperty(Snygg.Shape)
|
||||
@@ -170,14 +166,15 @@ class SnyggPropertySetEditor(initProperties: Map<String, SnyggValue>? = null) {
|
||||
}
|
||||
|
||||
fun rectangleShape(): SnyggRectangleShapeValue {
|
||||
return SnyggRectangleShapeValue(RectangleShape)
|
||||
return SnyggRectangleShapeValue()
|
||||
}
|
||||
|
||||
fun cutCornerShape(cornerSize: Dp): SnyggCutCornerShapeDpValue {
|
||||
return SnyggCutCornerShapeDpValue(
|
||||
shape = CutCornerShape(cornerSize),
|
||||
cornerSize, cornerSize, cornerSize, cornerSize,
|
||||
)
|
||||
fun circleShape(): SnyggCircleShapeValue {
|
||||
return SnyggCircleShapeValue()
|
||||
}
|
||||
|
||||
fun cutCornerShape(cornerSize: Dp): SnyggCutCornerDpShapeValue {
|
||||
return SnyggCutCornerDpShapeValue(cornerSize, cornerSize, cornerSize, cornerSize)
|
||||
}
|
||||
|
||||
fun cutCornerShape(
|
||||
@@ -185,18 +182,12 @@ class SnyggPropertySetEditor(initProperties: Map<String, SnyggValue>? = null) {
|
||||
topEnd: Dp,
|
||||
bottomEnd: Dp,
|
||||
bottomStart: Dp,
|
||||
): SnyggCutCornerShapeDpValue {
|
||||
return SnyggCutCornerShapeDpValue(
|
||||
shape = CutCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
): SnyggCutCornerDpShapeValue {
|
||||
return SnyggCutCornerDpShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
|
||||
fun cutCornerShape(cornerSize: Int): SnyggCutCornerShapePercentValue {
|
||||
return SnyggCutCornerShapePercentValue(
|
||||
shape = CutCornerShape(cornerSize),
|
||||
cornerSize, cornerSize, cornerSize, cornerSize,
|
||||
)
|
||||
fun cutCornerShape(cornerSize: Int): SnyggCutCornerPercentShapeValue {
|
||||
return SnyggCutCornerPercentShapeValue(cornerSize, cornerSize, cornerSize, cornerSize)
|
||||
}
|
||||
|
||||
fun cutCornerShape(
|
||||
@@ -204,18 +195,12 @@ class SnyggPropertySetEditor(initProperties: Map<String, SnyggValue>? = null) {
|
||||
topEnd: Int,
|
||||
bottomEnd: Int,
|
||||
bottomStart: Int,
|
||||
): SnyggCutCornerShapePercentValue {
|
||||
return SnyggCutCornerShapePercentValue(
|
||||
shape = CutCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
): SnyggCutCornerPercentShapeValue {
|
||||
return SnyggCutCornerPercentShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
|
||||
fun roundedCornerShape(cornerSize: Dp): SnyggRoundedCornerShapeDpValue {
|
||||
return SnyggRoundedCornerShapeDpValue(
|
||||
shape = RoundedCornerShape(cornerSize),
|
||||
cornerSize, cornerSize, cornerSize, cornerSize,
|
||||
)
|
||||
fun roundedCornerShape(cornerSize: Dp): SnyggRoundedCornerDpShapeValue {
|
||||
return SnyggRoundedCornerDpShapeValue(cornerSize, cornerSize, cornerSize, cornerSize)
|
||||
}
|
||||
|
||||
fun roundedCornerShape(
|
||||
@@ -223,18 +208,12 @@ class SnyggPropertySetEditor(initProperties: Map<String, SnyggValue>? = null) {
|
||||
topEnd: Dp,
|
||||
bottomEnd: Dp,
|
||||
bottomStart: Dp,
|
||||
): SnyggRoundedCornerShapeDpValue {
|
||||
return SnyggRoundedCornerShapeDpValue(
|
||||
shape = RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
): SnyggRoundedCornerDpShapeValue {
|
||||
return SnyggRoundedCornerDpShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
|
||||
fun roundedCornerShape(cornerSize: Int): SnyggRoundedCornerShapePercentValue {
|
||||
return SnyggRoundedCornerShapePercentValue(
|
||||
shape = RoundedCornerShape(cornerSize),
|
||||
cornerSize, cornerSize, cornerSize, cornerSize,
|
||||
)
|
||||
fun roundedCornerShape(cornerSize: Int): SnyggRoundedCornerPercentShapeValue {
|
||||
return SnyggRoundedCornerPercentShapeValue(cornerSize, cornerSize, cornerSize, cornerSize)
|
||||
}
|
||||
|
||||
fun roundedCornerShape(
|
||||
@@ -242,11 +221,8 @@ class SnyggPropertySetEditor(initProperties: Map<String, SnyggValue>? = null) {
|
||||
topEnd: Int,
|
||||
bottomEnd: Int,
|
||||
bottomStart: Int,
|
||||
): SnyggRoundedCornerShapePercentValue {
|
||||
return SnyggRoundedCornerShapePercentValue(
|
||||
shape = RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
): SnyggRoundedCornerPercentShapeValue {
|
||||
return SnyggRoundedCornerPercentShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
|
||||
fun size(dp: Dp): SnyggDpSizeValue {
|
||||
|
||||
@@ -16,46 +16,46 @@
|
||||
|
||||
package dev.patrickgold.florisboard.snygg
|
||||
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import dev.patrickgold.florisboard.common.kotlin.curlyFormat
|
||||
import dev.patrickgold.florisboard.ime.text.key.InputMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlin.Comparator
|
||||
|
||||
private const val ANNOTATION_MARKER = '@'
|
||||
|
||||
private const val ATTRIBUTE_OPEN = '['
|
||||
private const val ATTRIBUTE_CLOSE = ']'
|
||||
private const val ATTRIBUTE_ASSIGN = '='
|
||||
private const val ATTRIBUTE_OR = '|'
|
||||
private const val CODES_KEY = "code"
|
||||
private const val GROUPS_KEY = "group"
|
||||
private const val MODES_KEY = "mode"
|
||||
|
||||
private const val SELECTOR_COLON = ':'
|
||||
private const val PRESSED_SELECTOR = "pressed"
|
||||
private const val FOCUS_SELECTOR = "focus"
|
||||
private const val DISABLED_SELECTOR = "disabled"
|
||||
|
||||
@Serializable(with = SnyggRuleSerializer::class)
|
||||
@Serializable(with = SnyggRule.Serializer::class)
|
||||
data class SnyggRule(
|
||||
val isAnnotation: Boolean = false,
|
||||
val element: String,
|
||||
val codes: List<Int> = listOf(),
|
||||
val groups: List<Int> = listOf(),
|
||||
val modes: List<Int> = listOf(),
|
||||
val codes: List<Int> = emptyList(),
|
||||
val groups: List<Int> = emptyList(),
|
||||
val modes: List<Int> = emptyList(),
|
||||
val pressedSelector: Boolean = false,
|
||||
val focusSelector: Boolean = false,
|
||||
val disabledSelector: Boolean = false,
|
||||
) : Comparable<SnyggRule> {
|
||||
|
||||
companion object {
|
||||
const val ANNOTATION_MARKER = '@'
|
||||
|
||||
const val ATTRIBUTE_OPEN = '['
|
||||
const val ATTRIBUTE_CLOSE = ']'
|
||||
const val ATTRIBUTE_ASSIGN = '='
|
||||
const val ATTRIBUTE_OR = '|'
|
||||
const val CODES_KEY = "code"
|
||||
const val GROUPS_KEY = "group"
|
||||
const val MODES_KEY = "mode"
|
||||
|
||||
const val SELECTOR_COLON = ':'
|
||||
const val PRESSED_SELECTOR = "pressed"
|
||||
const val FOCUS_SELECTOR = "focus"
|
||||
const val DISABLED_SELECTOR = "disabled"
|
||||
|
||||
@Suppress("RegExpRedundantEscape", "RegExpSingleCharAlternation")
|
||||
private val RuleValidator =
|
||||
"""^(@?)[a-zA-Z0-9-]+(\[(code|group|mode)=(\+|-)?([0-9]+)(\|(\+|-)?([0-9]+))*\])*(:(pressed|focus|disabled))*${'$'}""".toRegex()
|
||||
@@ -69,18 +69,10 @@ data class SnyggRule(
|
||||
"m:capslock" to InputMode.CAPS_LOCK.value,
|
||||
)
|
||||
|
||||
val Comparator = Comparator<SnyggRule> { a, b ->
|
||||
when {
|
||||
a.isAnnotation && !b.isAnnotation -> -1
|
||||
!a.isAnnotation && b.isAnnotation -> 1
|
||||
else /* a.isAnnotation == b.isAnnotation */ -> {
|
||||
when (val elem = a.element.compareTo(b.element)) {
|
||||
0 -> a.comparatorWeight() - b.comparatorWeight()
|
||||
else -> elem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val Saver = Saver<SnyggRule?, String>(
|
||||
save = { it?.toString() ?: "" },
|
||||
restore = { from(it) },
|
||||
)
|
||||
|
||||
fun from(raw: String): SnyggRule? {
|
||||
val str = raw.trim().curlyFormat { placeholders[it]?.toString() }
|
||||
@@ -131,10 +123,6 @@ data class SnyggRule(
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: SnyggRule): Int {
|
||||
return Comparator.compare(this, other)
|
||||
}
|
||||
|
||||
override fun toString() = buildString {
|
||||
if (isAnnotation) {
|
||||
append(ANNOTATION_MARKER)
|
||||
@@ -148,26 +136,6 @@ data class SnyggRule(
|
||||
appendSelector(DISABLED_SELECTOR, disabledSelector)
|
||||
}
|
||||
|
||||
fun edit() = SnyggRuleEditor(
|
||||
isAnnotation,
|
||||
element,
|
||||
codes.toMutableList(),
|
||||
groups.toMutableList(),
|
||||
modes.toMutableList(),
|
||||
pressedSelector,
|
||||
focusSelector,
|
||||
disabledSelector,
|
||||
)
|
||||
|
||||
private fun comparatorWeight(): Int {
|
||||
return (if (codes.isNotEmpty()) 0x01 else 0) +
|
||||
(if (groups.isNotEmpty()) 0x02 else 0) +
|
||||
(if (modes.isNotEmpty()) 0x04 else 0) +
|
||||
(if (pressedSelector) 0x08 else 0) +
|
||||
(if (focusSelector) 0x10 else 0) +
|
||||
(if (disabledSelector) 0x20 else 0)
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendAttribute(key: String, entries: List<Int>) {
|
||||
if (entries.isNotEmpty()) {
|
||||
append(ATTRIBUTE_OPEN)
|
||||
@@ -189,34 +157,106 @@ data class SnyggRule(
|
||||
append(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SnyggRuleEditor(
|
||||
var isAnnotation: Boolean = false,
|
||||
var element: String = "",
|
||||
val codes: MutableList<Int> = mutableListOf(),
|
||||
val groups: MutableList<Int> = mutableListOf(),
|
||||
val modes: MutableList<Int> = mutableListOf(),
|
||||
var pressedSelector: Boolean = false,
|
||||
var focusSelector: Boolean = false,
|
||||
var disabledSelector: Boolean = false,
|
||||
) {
|
||||
fun build() = SnyggRule(
|
||||
isAnnotation, element, codes, groups, modes,
|
||||
pressedSelector, focusSelector, disabledSelector,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializer(forClass = SnyggRule::class)
|
||||
class SnyggRuleSerializer : KSerializer<SnyggRule> {
|
||||
override val descriptor = PrimitiveSerialDescriptor("SnyggRule", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: SnyggRule) {
|
||||
encoder.encodeString(value.toString())
|
||||
override fun compareTo(other: SnyggRule): Int {
|
||||
return when {
|
||||
this.isAnnotation && !other.isAnnotation -> -1
|
||||
!this.isAnnotation && other.isAnnotation -> 1
|
||||
else -> when (val elem = this.element.compareTo(other.element)) {
|
||||
0 -> when (val diff = this.comparatorWeight() - other.comparatorWeight()) {
|
||||
0 -> when {
|
||||
this.codes.size != other.codes.size -> this.codes.size.compareTo(other.codes.size)
|
||||
this.groups.size != other.groups.size -> this.groups.size.compareTo(other.groups.size)
|
||||
this.modes.size != other.modes.size -> this.modes.size.compareTo(other.modes.size)
|
||||
else -> {
|
||||
this.codes.indices.firstNotNullOfOrNull { n ->
|
||||
(this.codes[n].compareTo(other.codes[n])).takeIf { it != 0 }
|
||||
} ?: this.groups.indices.firstNotNullOfOrNull { n ->
|
||||
(this.groups[n].compareTo(other.groups[n])).takeIf { it != 0 }
|
||||
} ?: this.modes.indices.firstNotNullOfOrNull { n ->
|
||||
(this.modes[n].compareTo(other.modes[n])).takeIf { it != 0 }
|
||||
} ?: 0
|
||||
}
|
||||
}
|
||||
else -> diff
|
||||
}
|
||||
else -> when {
|
||||
this.element == FlorisImeUi.Keyboard -> -1
|
||||
other.element == FlorisImeUi.Keyboard -> 1
|
||||
this.element == FlorisImeUi.Key -> -1
|
||||
other.element == FlorisImeUi.Key -> 1
|
||||
this.element == FlorisImeUi.KeyHint -> -1
|
||||
other.element == FlorisImeUi.KeyHint -> 1
|
||||
this.element == FlorisImeUi.KeyPopup -> -1
|
||||
other.element == FlorisImeUi.KeyPopup -> 1
|
||||
else -> elem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): SnyggRule {
|
||||
return SnyggRule.from(decoder.decodeString()) ?: SnyggRule(element = "invalid")
|
||||
private fun comparatorWeight(): Int {
|
||||
return (if (codes.isNotEmpty()) 0x01 else 0) +
|
||||
(if (groups.isNotEmpty()) 0x02 else 0) +
|
||||
(if (modes.isNotEmpty()) 0x04 else 0) +
|
||||
(if (pressedSelector) 0x08 else 0) +
|
||||
(if (focusSelector) 0x10 else 0) +
|
||||
(if (disabledSelector) 0x20 else 0)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SnyggRule
|
||||
|
||||
if (isAnnotation != other.isAnnotation) return false
|
||||
if (element != other.element) return false
|
||||
if (!codes.containsAll(other.codes) || !other.codes.containsAll(codes)) return false
|
||||
if (!groups.containsAll(other.groups) || !other.groups.containsAll(groups)) return false
|
||||
if (!modes.containsAll(other.modes) || !other.modes.containsAll(modes)) return false
|
||||
if (pressedSelector != other.pressedSelector) return false
|
||||
if (focusSelector != other.focusSelector) return false
|
||||
if (disabledSelector != other.disabledSelector) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = isAnnotation.hashCode()
|
||||
result = 31 * result + element.hashCode()
|
||||
for (code in codes.sorted()) {
|
||||
result = 31 * result + code.hashCode()
|
||||
}
|
||||
for (group in groups.sorted()) {
|
||||
result = 31 * result + group.hashCode()
|
||||
}
|
||||
for (mode in modes.sorted()) {
|
||||
result = 31 * result + mode.hashCode()
|
||||
}
|
||||
result = 31 * result + pressedSelector.hashCode()
|
||||
result = 31 * result + focusSelector.hashCode()
|
||||
result = 31 * result + disabledSelector.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
object Serializer : KSerializer<SnyggRule> {
|
||||
override val descriptor = PrimitiveSerialDescriptor("SnyggRule", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: SnyggRule) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): SnyggRule {
|
||||
return from(decoder.decodeString()) ?: SnyggRule(element = "invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SnyggRule.Companion.definedVariablesRule(): SnyggRule {
|
||||
return SnyggRule(isAnnotation = true, element = "defines")
|
||||
}
|
||||
|
||||
fun SnyggRule.isDefinedVariablesRule(): Boolean {
|
||||
return this.isAnnotation && this.element == "defines"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import dev.patrickgold.florisboard.snygg.value.SnyggExplicitInheritValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValueEncoder
|
||||
|
||||
open class SnyggSpec(init: SnyggSpecBuilder.() -> Unit) {
|
||||
private val elements: Map<String, SnyggPropertySetSpec>
|
||||
val elements: Map<String, SnyggPropertySetSpec>
|
||||
|
||||
init {
|
||||
val builder = SnyggSpecBuilder()
|
||||
|
||||
@@ -29,6 +29,7 @@ import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.*
|
||||
|
||||
val SnyggStylesheetJsonConfig = Json
|
||||
|
||||
@@ -182,9 +183,7 @@ class SnyggStylesheet(
|
||||
}
|
||||
|
||||
fun edit(): SnyggStylesheetEditor {
|
||||
val ruleMap = rules
|
||||
.mapKeys { (rule, _) -> rule.edit() }
|
||||
.mapValues { (_, propertySet) -> propertySet.edit() }
|
||||
val ruleMap = rules.mapValues { (_, propertySet) -> propertySet.edit() }
|
||||
return SnyggStylesheetEditor(ruleMap)
|
||||
}
|
||||
}
|
||||
@@ -195,8 +194,8 @@ fun SnyggStylesheet(stylesheetBlock: SnyggStylesheetEditor.() -> Unit): SnyggSty
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
class SnyggStylesheetEditor(initRules: Map<SnyggRuleEditor, SnyggPropertySetEditor>? = null){
|
||||
val rules = mutableMapOf<SnyggRuleEditor, SnyggPropertySetEditor>()
|
||||
class SnyggStylesheetEditor(initRules: Map<SnyggRule, SnyggPropertySetEditor>? = null){
|
||||
val rules = sortedMapOf<SnyggRule, SnyggPropertySetEditor>()
|
||||
|
||||
init {
|
||||
if (initRules != null) {
|
||||
@@ -207,11 +206,11 @@ class SnyggStylesheetEditor(initRules: Map<SnyggRuleEditor, SnyggPropertySetEdit
|
||||
fun annotation(name: String, propertySetBlock: SnyggPropertySetEditor.() -> Unit) {
|
||||
val propertySetEditor = SnyggPropertySetEditor()
|
||||
propertySetBlock(propertySetEditor)
|
||||
val ruleEditor = SnyggRuleEditor(
|
||||
val rule = SnyggRule(
|
||||
isAnnotation = true,
|
||||
element = name,
|
||||
)
|
||||
rules[ruleEditor] = propertySetEditor
|
||||
rules[rule] = propertySetEditor
|
||||
}
|
||||
|
||||
fun defines(propertySetBlock: SnyggPropertySetEditor.() -> Unit) {
|
||||
@@ -229,7 +228,7 @@ class SnyggStylesheetEditor(initRules: Map<SnyggRuleEditor, SnyggPropertySetEdit
|
||||
) {
|
||||
val propertySetEditor = SnyggPropertySetEditor()
|
||||
propertySetBlock(propertySetEditor)
|
||||
val ruleEditor = SnyggRuleEditor(
|
||||
val rule = SnyggRule(
|
||||
isAnnotation = false,
|
||||
element = this,
|
||||
codes.toMutableList(),
|
||||
@@ -239,13 +238,11 @@ class SnyggStylesheetEditor(initRules: Map<SnyggRuleEditor, SnyggPropertySetEdit
|
||||
focusSelector,
|
||||
disabledSelector,
|
||||
)
|
||||
rules[ruleEditor] = propertySetEditor
|
||||
rules[rule] = propertySetEditor
|
||||
}
|
||||
|
||||
fun build(isFullyQualified: Boolean = false): SnyggStylesheet {
|
||||
val rulesMap = rules
|
||||
.mapKeys { (ruleEditor, _) -> ruleEditor.build() }
|
||||
.mapValues { (_, propertySetEditor) -> propertySetEditor.build() }
|
||||
val rulesMap = rules.mapValues { (_, propertySetEditor) -> propertySetEditor.build() }
|
||||
return SnyggStylesheet(rulesMap, isFullyQualified)
|
||||
}
|
||||
}
|
||||
@@ -271,7 +268,7 @@ class SnyggStylesheetSerializer : KSerializer<SnyggStylesheet> {
|
||||
for ((rule, rawProperties) in rawRuleMap) {
|
||||
// FIXME: hardcoding which spec to use, the selection should happen dynamically
|
||||
val stylesheetSpec = FlorisImeUiSpec
|
||||
if (rule.isAnnotation && rule.element == "defines") {
|
||||
if (rule.isDefinedVariablesRule()) {
|
||||
val parsedProperties = rawProperties.mapValues { (_, rawValue) ->
|
||||
SnyggVarValueEncoders.firstNotNullOfOrNull { it.deserialize(rawValue).getOrNull() }
|
||||
?: SnyggImplicitInheritValue
|
||||
|
||||
@@ -19,11 +19,13 @@ package dev.patrickgold.florisboard.snygg.ui
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import dev.patrickgold.florisboard.snygg.SnyggPropertySet
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggDpSizeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggShapeValue
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggSolidColorValue
|
||||
@@ -43,16 +45,25 @@ fun Modifier.snyggBackground(
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.snyggShadow(
|
||||
style: SnyggPropertySet,
|
||||
elevation: Dp = style.shadowElevation.dpSize(),
|
||||
shape: Shape = style.shape.shape(),
|
||||
): Modifier {
|
||||
// TODO: find a performant way to implement shadow color
|
||||
return this.shadow(elevation, shape, clip = false)
|
||||
}
|
||||
|
||||
fun Modifier.snyggClip(
|
||||
shape: SnyggValue,
|
||||
): Modifier {
|
||||
return this.clip(shape.shape())
|
||||
}
|
||||
|
||||
fun SnyggValue.solidColor(): Color {
|
||||
fun SnyggValue.solidColor(default: Color = Color.Transparent): Color {
|
||||
return when (this) {
|
||||
is SnyggSolidColorValue -> this.color
|
||||
else -> Color.Transparent
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.snygg.value.SnyggValue
|
||||
|
||||
@@ -50,15 +49,16 @@ fun SnyggSurface(
|
||||
border: BorderStroke? = null,
|
||||
clip: Boolean = false,
|
||||
contentPadding: PaddingValues = NoContentPadding,
|
||||
elevation: Dp = 0.dp,
|
||||
elevation: SnyggValue? = null,
|
||||
clickAndSemanticsModifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val elevationDp = elevation?.dpSize() ?: 0.dp
|
||||
val shapeValue = shape?.shape() ?: RectangleShape
|
||||
val color = background.solidColor()
|
||||
val contentColor = contentColorFor(color)
|
||||
val elevationOverlay = LocalElevationOverlay.current
|
||||
val absoluteElevation = LocalAbsoluteElevation.current + elevation
|
||||
val absoluteElevation = LocalAbsoluteElevation.current + elevationDp
|
||||
val backgroundColor = if (color == MaterialTheme.colors.surface && elevationOverlay != null) {
|
||||
elevationOverlay.apply(color, absoluteElevation)
|
||||
} else {
|
||||
@@ -70,7 +70,7 @@ fun SnyggSurface(
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
.shadow(elevation, shapeValue, clip = false)
|
||||
.shadow(elevationDp, shapeValue, clip = false)
|
||||
.then(if (border != null) Modifier.border(border, shapeValue) else Modifier)
|
||||
.then(if (clip) Modifier.clip(shapeValue) else Modifier)
|
||||
.background(
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package dev.patrickgold.florisboard.snygg.value
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
sealed interface SnyggAppearanceValue : SnyggValue
|
||||
|
||||
@@ -83,12 +84,14 @@ data class SnyggSolidColorValue(val color: Color) : SnyggAppearanceValue {
|
||||
},
|
||||
)
|
||||
|
||||
override fun defaultValue() = SnyggSolidColorValue(Color.Black)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggSolidColorValue)
|
||||
val map = SnyggIdToValueMap.new(
|
||||
RgbaColor.RedId to (v.color.red * RgbaColor.RedMax),
|
||||
RgbaColor.GreenId to (v.color.green * RgbaColor.GreenMax),
|
||||
RgbaColor.BlueId to (v.color.blue * RgbaColor.BlueMax),
|
||||
RgbaColor.RedId to (v.color.red * RgbaColor.RedMax).roundToInt(),
|
||||
RgbaColor.GreenId to (v.color.green * RgbaColor.GreenMax).roundToInt(),
|
||||
RgbaColor.BlueId to (v.color.blue * RgbaColor.BlueMax).roundToInt(),
|
||||
RgbaColor.AlphaId to v.color.alpha,
|
||||
)
|
||||
return@runCatching spec.pack(map)
|
||||
|
||||
@@ -28,6 +28,8 @@ data class SnyggImageRefValue(val relPath: String) : SnyggRefValue {
|
||||
function(name = ImageFunction) { string(id = RelPath, regex = RefMatcher) }
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggImageRefValue("")
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggImageRefValue)
|
||||
val map = SnyggIdToValueMap.new(RelPath to v.relPath)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package dev.patrickgold.florisboard.snygg.value
|
||||
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
@@ -29,6 +30,7 @@ private const val CornerSizeBottomEnd = "cornerSizeBottomEnd"
|
||||
private const val CornerSizeBottomStart = "cornerSizeBottomStart"
|
||||
|
||||
private const val Rectangle = "rectangle"
|
||||
private const val Circle = "circle"
|
||||
private const val CutCorner = "cut-corner"
|
||||
private const val RoundedCorner = "rounded-corner"
|
||||
|
||||
@@ -38,12 +40,30 @@ sealed interface SnyggShapeValue : SnyggValue {
|
||||
val shape: Shape
|
||||
}
|
||||
|
||||
data class SnyggRectangleShapeValue(override val shape: Shape) : SnyggShapeValue {
|
||||
sealed interface SnyggDpShapeValue : SnyggShapeValue {
|
||||
override val shape: Shape
|
||||
val topStart: Dp
|
||||
val topEnd: Dp
|
||||
val bottomEnd: Dp
|
||||
val bottomStart: Dp
|
||||
}
|
||||
|
||||
sealed interface SnyggPercentShapeValue : SnyggShapeValue {
|
||||
override val shape: Shape
|
||||
val topStart: Int
|
||||
val topEnd: Int
|
||||
val bottomEnd: Int
|
||||
val bottomStart: Int
|
||||
}
|
||||
|
||||
data class SnyggRectangleShapeValue(override val shape: Shape = RectangleShape) : SnyggShapeValue {
|
||||
companion object : SnyggValueEncoder {
|
||||
override val spec = SnyggValueSpec {
|
||||
function(Rectangle) { nothing() }
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggRectangleShapeValue()
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggRectangleShapeValue)
|
||||
val map = SnyggIdToValueMap.new()
|
||||
@@ -53,20 +73,44 @@ data class SnyggRectangleShapeValue(override val shape: Shape) : SnyggShapeValue
|
||||
override fun deserialize(v: String) = runCatching<SnyggValue> {
|
||||
val map = SnyggIdToValueMap.new()
|
||||
spec.parse(v, map)
|
||||
return@runCatching SnyggRectangleShapeValue(shape = RectangleShape)
|
||||
return@runCatching SnyggRectangleShapeValue()
|
||||
}
|
||||
}
|
||||
|
||||
override fun encoder() = Companion
|
||||
}
|
||||
|
||||
data class SnyggCutCornerShapeDpValue(
|
||||
override val shape: CutCornerShape,
|
||||
val topStart: Dp,
|
||||
val topEnd: Dp,
|
||||
val bottomEnd: Dp,
|
||||
val bottomStart: Dp,
|
||||
) : SnyggShapeValue {
|
||||
data class SnyggCircleShapeValue(override val shape: Shape = CircleShape) : SnyggShapeValue {
|
||||
companion object : SnyggValueEncoder {
|
||||
override val spec = SnyggValueSpec {
|
||||
function(Circle) { nothing() }
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggCircleShapeValue()
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggCircleShapeValue)
|
||||
val map = SnyggIdToValueMap.new()
|
||||
return@runCatching spec.pack(map)
|
||||
}
|
||||
|
||||
override fun deserialize(v: String) = runCatching<SnyggValue> {
|
||||
val map = SnyggIdToValueMap.new()
|
||||
spec.parse(v, map)
|
||||
return@runCatching SnyggCircleShapeValue()
|
||||
}
|
||||
}
|
||||
|
||||
override fun encoder() = Companion
|
||||
}
|
||||
|
||||
data class SnyggCutCornerDpShapeValue(
|
||||
override val topStart: Dp,
|
||||
override val topEnd: Dp,
|
||||
override val bottomEnd: Dp,
|
||||
override val bottomStart: Dp,
|
||||
override val shape: CutCornerShape = CutCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
) : SnyggDpShapeValue {
|
||||
companion object : SnyggValueEncoder {
|
||||
override val spec = SnyggValueSpec {
|
||||
function(CutCorner) {
|
||||
@@ -79,8 +123,10 @@ data class SnyggCutCornerShapeDpValue(
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggCutCornerDpShapeValue(0.dp, 0.dp, 0.dp, 0.dp)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggCutCornerShapeDpValue)
|
||||
require(v is SnyggCutCornerDpShapeValue)
|
||||
val map = SnyggIdToValueMap.new(
|
||||
CornerSizeTopStart to v.topStart.value,
|
||||
CornerSizeTopEnd to v.topEnd.value,
|
||||
@@ -97,23 +143,20 @@ data class SnyggCutCornerShapeDpValue(
|
||||
val topEnd = map.getOrThrow<Int>(CornerSizeTopEnd).dp
|
||||
val bottomEnd = map.getOrThrow<Int>(CornerSizeBottomEnd).dp
|
||||
val bottomStart = map.getOrThrow<Int>(CornerSizeBottomStart).dp
|
||||
return@runCatching SnyggCutCornerShapeDpValue(
|
||||
shape = CutCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
return@runCatching SnyggCutCornerDpShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
}
|
||||
|
||||
override fun encoder() = Companion
|
||||
}
|
||||
|
||||
data class SnyggCutCornerShapePercentValue(
|
||||
override val shape: CutCornerShape,
|
||||
val topStart: Int,
|
||||
val topEnd: Int,
|
||||
val bottomEnd: Int,
|
||||
val bottomStart: Int,
|
||||
) : SnyggShapeValue {
|
||||
data class SnyggCutCornerPercentShapeValue(
|
||||
override val topStart: Int,
|
||||
override val topEnd: Int,
|
||||
override val bottomEnd: Int,
|
||||
override val bottomStart: Int,
|
||||
override val shape: CutCornerShape = CutCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
) : SnyggPercentShapeValue {
|
||||
companion object : SnyggValueEncoder {
|
||||
override val spec = SnyggValueSpec {
|
||||
function(CutCorner) {
|
||||
@@ -126,8 +169,10 @@ data class SnyggCutCornerShapePercentValue(
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggCutCornerPercentShapeValue(0, 0, 0, 0)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggCutCornerShapePercentValue)
|
||||
require(v is SnyggCutCornerPercentShapeValue)
|
||||
val map = SnyggIdToValueMap.new(
|
||||
CornerSizeTopStart to v.topStart,
|
||||
CornerSizeTopEnd to v.topEnd,
|
||||
@@ -144,23 +189,20 @@ data class SnyggCutCornerShapePercentValue(
|
||||
val topEnd = map.getOrThrow<Int>(CornerSizeTopEnd)
|
||||
val bottomEnd = map.getOrThrow<Int>(CornerSizeBottomEnd)
|
||||
val bottomStart = map.getOrThrow<Int>(CornerSizeBottomStart)
|
||||
return@runCatching SnyggCutCornerShapePercentValue(
|
||||
shape = CutCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
return@runCatching SnyggCutCornerPercentShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
}
|
||||
|
||||
override fun encoder() = Companion
|
||||
}
|
||||
|
||||
data class SnyggRoundedCornerShapeDpValue(
|
||||
override val shape: RoundedCornerShape,
|
||||
val topStart: Dp,
|
||||
val topEnd: Dp,
|
||||
val bottomEnd: Dp,
|
||||
val bottomStart: Dp,
|
||||
) : SnyggShapeValue {
|
||||
data class SnyggRoundedCornerDpShapeValue(
|
||||
override val topStart: Dp,
|
||||
override val topEnd: Dp,
|
||||
override val bottomEnd: Dp,
|
||||
override val bottomStart: Dp,
|
||||
override val shape: RoundedCornerShape = RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
) : SnyggDpShapeValue {
|
||||
companion object : SnyggValueEncoder {
|
||||
override val spec = SnyggValueSpec {
|
||||
function(RoundedCorner) {
|
||||
@@ -173,8 +215,10 @@ data class SnyggRoundedCornerShapeDpValue(
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggRoundedCornerDpShapeValue(0.dp, 0.dp, 0.dp, 0.dp)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggRoundedCornerShapeDpValue)
|
||||
require(v is SnyggRoundedCornerDpShapeValue)
|
||||
val map = SnyggIdToValueMap.new(
|
||||
CornerSizeTopStart to v.topStart.value,
|
||||
CornerSizeTopEnd to v.topEnd.value,
|
||||
@@ -191,23 +235,20 @@ data class SnyggRoundedCornerShapeDpValue(
|
||||
val topEnd = map.getOrThrow<Float>(CornerSizeTopEnd).dp
|
||||
val bottomEnd = map.getOrThrow<Float>(CornerSizeBottomEnd).dp
|
||||
val bottomStart = map.getOrThrow<Float>(CornerSizeBottomStart).dp
|
||||
return@runCatching SnyggRoundedCornerShapeDpValue(
|
||||
shape = RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
return@runCatching SnyggRoundedCornerDpShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
}
|
||||
|
||||
override fun encoder() = Companion
|
||||
}
|
||||
|
||||
data class SnyggRoundedCornerShapePercentValue(
|
||||
override val shape: RoundedCornerShape,
|
||||
val topStart: Int,
|
||||
val topEnd: Int,
|
||||
val bottomEnd: Int,
|
||||
val bottomStart: Int,
|
||||
) : SnyggShapeValue {
|
||||
data class SnyggRoundedCornerPercentShapeValue(
|
||||
override val topStart: Int,
|
||||
override val topEnd: Int,
|
||||
override val bottomEnd: Int,
|
||||
override val bottomStart: Int,
|
||||
override val shape: RoundedCornerShape = RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
) : SnyggPercentShapeValue {
|
||||
companion object : SnyggValueEncoder {
|
||||
override val spec = SnyggValueSpec {
|
||||
function(RoundedCorner) {
|
||||
@@ -220,8 +261,10 @@ data class SnyggRoundedCornerShapePercentValue(
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggRoundedCornerPercentShapeValue(0, 0, 0, 0)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggRoundedCornerShapePercentValue)
|
||||
require(v is SnyggRoundedCornerPercentShapeValue)
|
||||
val map = SnyggIdToValueMap.new(
|
||||
CornerSizeTopStart to v.topStart,
|
||||
CornerSizeTopEnd to v.topEnd,
|
||||
@@ -238,10 +281,7 @@ data class SnyggRoundedCornerShapePercentValue(
|
||||
val topEnd = map.getOrThrow<Int>(CornerSizeTopEnd)
|
||||
val bottomEnd = map.getOrThrow<Int>(CornerSizeBottomEnd)
|
||||
val bottomStart = map.getOrThrow<Int>(CornerSizeBottomStart)
|
||||
return@runCatching SnyggRoundedCornerShapePercentValue(
|
||||
shape = RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart),
|
||||
topStart, topEnd, bottomEnd, bottomStart,
|
||||
)
|
||||
return@runCatching SnyggRoundedCornerPercentShapeValue(topStart, topEnd, bottomEnd, bottomStart)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ data class SnyggDpSizeValue(val dp: Dp) : SnyggSizeValue {
|
||||
float(id = Size, unit = DpUnit, min = 0.0f)
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggDpSizeValue(0.dp)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggDpSizeValue)
|
||||
val map = SnyggIdToValueMap.new(Size to v.dp.value)
|
||||
@@ -53,9 +55,11 @@ data class SnyggDpSizeValue(val dp: Dp) : SnyggSizeValue {
|
||||
data class SnyggSpSizeValue(val sp: TextUnit) : SnyggSizeValue {
|
||||
companion object : SnyggValueEncoder {
|
||||
override val spec = SnyggValueSpec {
|
||||
float(id = Size, unit = SpUnit, min = 0.0f)
|
||||
float(id = Size, unit = SpUnit, min = 1.0f)
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggSpSizeValue(24.sp)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggSpSizeValue)
|
||||
val map = SnyggIdToValueMap.new(Size to v.sp.value)
|
||||
@@ -79,6 +83,8 @@ data class SnyggPercentageSizeValue(val percentage: Float) : SnyggSizeValue {
|
||||
percentageFloat(id = Size)
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggPercentageSizeValue(0f)
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggPercentageSizeValue)
|
||||
val map = SnyggIdToValueMap.new(Size to v.percentage * 100.0f)
|
||||
|
||||
@@ -63,6 +63,8 @@ object SnyggExplicitInheritValue : SnyggValue, SnyggValueEncoder {
|
||||
keywords(keywords = listOf(Inherit))
|
||||
}
|
||||
|
||||
override fun defaultValue() = this
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
return@runCatching Inherit
|
||||
}
|
||||
@@ -90,6 +92,8 @@ object SnyggImplicitInheritValue : SnyggValue, SnyggValueEncoder {
|
||||
keywords(keywords = listOf(ImplicitInherit))
|
||||
}
|
||||
|
||||
override fun defaultValue() = this
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
error("Implicit inherit is not meant to be serialized")
|
||||
}
|
||||
@@ -105,10 +109,11 @@ val SnyggVarValueEncoders = listOf(
|
||||
SnyggSolidColorValue,
|
||||
SnyggImageRefValue,
|
||||
SnyggRectangleShapeValue,
|
||||
SnyggCutCornerShapeDpValue,
|
||||
SnyggCutCornerShapePercentValue,
|
||||
SnyggRoundedCornerShapeDpValue,
|
||||
SnyggRoundedCornerShapePercentValue,
|
||||
SnyggCircleShapeValue,
|
||||
SnyggRoundedCornerDpShapeValue,
|
||||
SnyggRoundedCornerPercentShapeValue,
|
||||
SnyggCutCornerDpShapeValue,
|
||||
SnyggCutCornerPercentShapeValue,
|
||||
SnyggDpSizeValue,
|
||||
SnyggSpSizeValue,
|
||||
SnyggPercentageSizeValue,
|
||||
|
||||
@@ -38,6 +38,11 @@ interface SnyggValueEncoder {
|
||||
val alternativeSpecs: List<SnyggValueSpec>
|
||||
get() = emptyList()
|
||||
|
||||
/**
|
||||
* Returns a default value for this encoder, used for dynamic theme building in a user interface.
|
||||
*/
|
||||
fun defaultValue(): SnyggValue
|
||||
|
||||
/**
|
||||
* Serialize given Snygg value [v] and return a String representation of it. This method is exception-free, which
|
||||
* means it must **never** throw an exception. If an error during serialization occurs, it should be returned as a
|
||||
|
||||
@@ -27,6 +27,8 @@ data class SnyggDefinedVarValue(val key: String) : SnyggVarValue {
|
||||
function(name = "var") { string(id = VarKey, regex = VarRegex) }
|
||||
}
|
||||
|
||||
override fun defaultValue() = SnyggDefinedVarValue("")
|
||||
|
||||
override fun serialize(v: SnyggValue) = runCatching<String> {
|
||||
require(v is SnyggDefinedVarValue)
|
||||
val map = SnyggIdToValueMap.new(VarKey to v.key)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~ Copyright (C) 2021 Patrick Goldinger
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#20000000">
|
||||
<item
|
||||
android:bottom="4dp"
|
||||
android:left="4dp"
|
||||
android:right="4dp"
|
||||
android:shape="rectangle"
|
||||
android:top="4dp">
|
||||
<shape>
|
||||
<corners android:radius="32dp" />
|
||||
<solid android:color="#1F000000" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:bottom="5dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp"
|
||||
android:shape="rectangle"
|
||||
android:top="5dp">
|
||||
<shape>
|
||||
<corners android:radius="32dp" />
|
||||
<solid android:color="#FFFFFFFF" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
5
app/src/main/res/drawable/ic_close.xml
Normal file
5
app/src/main/res/drawable/ic_close.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user