mirror of
https://github.com/florisboard/florisboard.git
synced 2024-09-19 19:42:20 +02:00
Merge pull request #2473 from florisboard/feat/addons-support
Add addons support
This commit is contained in:
commit
c909d3ad7d
@ -16,6 +16,8 @@ This includes, but is not exclusive to:
|
|||||||
- Remove existing glide/swipe typing (see 0.5 milestone)
|
- Remove existing glide/swipe typing (see 0.5 milestone)
|
||||||
- Improvements in clipboard / emoji functionality (v0.4.0-beta01/beta02)
|
- Improvements in clipboard / emoji functionality (v0.4.0-beta01/beta02)
|
||||||
- Prepare project to have native code implemented in [Rust](https://www.rust-lang.org/) (v0.4.0-beta02)
|
- Prepare project to have native code implemented in [Rust](https://www.rust-lang.org/) (v0.4.0-beta02)
|
||||||
|
- - Upgrade Settings UI to Material 3 (v0.4.0-beta03)
|
||||||
|
- Add support for importing extensions via system file handler APIs (relevant for Addons store) (v0.4.0-beta03)
|
||||||
|
|
||||||
Note that the previous versioning scheme has been dropped in favor of using a major.minor.patch versioning scheme, so versions like `0.3.16` are a thing of the past :)
|
Note that the previous versioning scheme has been dropped in favor of using a major.minor.patch versioning scheme, so versions like `0.3.16` are a thing of the past :)
|
||||||
|
|
||||||
@ -32,7 +34,6 @@ Note that the previous versioning scheme has been dropped in favor of using a ma
|
|||||||
- RFC document with technical details will be released later
|
- RFC document with technical details will be released later
|
||||||
- Add Tablet mode / Optimizations for landscape input based on new keyboard layout engine
|
- Add Tablet mode / Optimizations for landscape input based on new keyboard layout engine
|
||||||
- Reimplementation of glide typing with the new layout engine and predictive text core
|
- Reimplementation of glide typing with the new layout engine and predictive text core
|
||||||
- Add support for importing extensions via system file handler APIs (relevant for Addons store)
|
|
||||||
- Add support for any remaining new features introduced with Android 13
|
- Add support for any remaining new features introduced with Android 13
|
||||||
|
|
||||||
## 0.6
|
## 0.6
|
||||||
@ -52,7 +53,6 @@ Note that the previous versioning scheme has been dropped in favor of using a ma
|
|||||||
|
|
||||||
**Features that MAY be added (even in versions mentioned above) or dismissed**
|
**Features that MAY be added (even in versions mentioned above) or dismissed**
|
||||||
|
|
||||||
- Upgrade Settings UI to Material 3
|
|
||||||
- Full on-board layout editor which allows users to create their own layouts without writing a JSON file
|
- Full on-board layout editor which allows users to create their own layouts without writing a JSON file
|
||||||
- Theme rework part II
|
- Theme rework part II
|
||||||
- Adaptive themes v2
|
- Adaptive themes v2
|
||||||
|
@ -64,6 +64,8 @@ android {
|
|||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField("String", "BUILD_COMMIT_HASH", "\"${getGitCommitHash()}\"")
|
buildConfigField("String", "BUILD_COMMIT_HASH", "\"${getGitCommitHash()}\"")
|
||||||
|
buildConfigField("String", "FLADDONS_API_VERSION", "\"v~draft2\"")
|
||||||
|
buildConfigField("String", "FLADDONS_STORE_URL", "\"fladdonstest.patrickgold.dev\"")
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
@ -84,10 +84,23 @@
|
|||||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/FlorisAppTheme.Splash"
|
android:theme="@style/FlorisAppTheme.Splash"
|
||||||
android:exported="false">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<data android:scheme="florisboard" android:host="app-ui"/>
|
<data android:scheme="florisboard" android:host="app-ui"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:label="Import Extension">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:scheme="content"/>
|
||||||
|
<data android:mimeType="application/vnd.florisboard.extension+zip"/>
|
||||||
|
<data android:mimeType="application/octet-stream"/><!-- Firefox looking at you :eyes: -->
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:label="Import Extension">
|
||||||
|
<action android:name="android.intent.action.SEND"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:mimeType="application/vnd.florisboard.extension+zip"/>
|
||||||
|
<data android:mimeType="application/octet-stream"/><!-- Firefox looking at you :eyes: -->
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
|
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
|
||||||
@ -106,24 +119,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<!-- Import File Bridging Activity -->
|
|
||||||
<activity
|
|
||||||
android:name="dev.patrickgold.florisboard.app.ext.ImportFileActivity"
|
|
||||||
android:icon="@mipmap/floris_app_icon"
|
|
||||||
android:label="@string/settings__title"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
android:theme="@style/FlorisAppTheme"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<data android:scheme="*" android:host="*" android:pathPattern=".*\\.flex"/>
|
|
||||||
<data android:scheme="*" android:host="*" android:pathPattern=".*\\.xpi"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<!-- Crash Dialog Activity -->
|
<!-- Crash Dialog Activity -->
|
||||||
<activity
|
<activity
|
||||||
android:name="dev.patrickgold.florisboard.lib.crashutility.CrashDialogActivity"
|
android:name="dev.patrickgold.florisboard.lib.crashutility.CrashDialogActivity"
|
||||||
|
@ -40,7 +40,6 @@ import dev.patrickgold.florisboard.lib.devtools.Flog
|
|||||||
import dev.patrickgold.florisboard.lib.devtools.LogTopic
|
import dev.patrickgold.florisboard.lib.devtools.LogTopic
|
||||||
import dev.patrickgold.florisboard.lib.devtools.flogError
|
import dev.patrickgold.florisboard.lib.devtools.flogError
|
||||||
import dev.patrickgold.florisboard.lib.ext.ExtensionManager
|
import dev.patrickgold.florisboard.lib.ext.ExtensionManager
|
||||||
import dev.patrickgold.florisboard.lib.io.AssetManager
|
|
||||||
import dev.patrickgold.florisboard.lib.io.deleteContentsRecursively
|
import dev.patrickgold.florisboard.lib.io.deleteContentsRecursively
|
||||||
import dev.patrickgold.jetpref.datastore.JetPref
|
import dev.patrickgold.jetpref.datastore.JetPref
|
||||||
import org.florisboard.lib.kotlin.tryOrNull
|
import org.florisboard.lib.kotlin.tryOrNull
|
||||||
@ -67,7 +66,6 @@ class FlorisApplication : Application() {
|
|||||||
private val prefs by florisPreferenceModel()
|
private val prefs by florisPreferenceModel()
|
||||||
private val mainHandler by lazy { Handler(mainLooper) }
|
private val mainHandler by lazy { Handler(mainLooper) }
|
||||||
|
|
||||||
val assetManager = lazy { AssetManager(this) }
|
|
||||||
val cacheManager = lazy { CacheManager(this) }
|
val cacheManager = lazy { CacheManager(this) }
|
||||||
val clipboardManager = lazy { ClipboardManager(this) }
|
val clipboardManager = lazy { ClipboardManager(this) }
|
||||||
val editorInstance = lazy { EditorInstance(this) }
|
val editorInstance = lazy { EditorInstance(this) }
|
||||||
@ -144,8 +142,6 @@ private tailrec fun Context.florisApplication(): FlorisApplication {
|
|||||||
|
|
||||||
fun Context.appContext() = lazyOf(this.florisApplication())
|
fun Context.appContext() = lazyOf(this.florisApplication())
|
||||||
|
|
||||||
fun Context.assetManager() = this.florisApplication().assetManager
|
|
||||||
|
|
||||||
fun Context.cacheManager() = this.florisApplication().cacheManager
|
fun Context.cacheManager() = this.florisApplication().cacheManager
|
||||||
|
|
||||||
fun Context.clipboardManager() = this.florisApplication().clipboardManager
|
fun Context.clipboardManager() = this.florisApplication().clipboardManager
|
||||||
|
@ -642,19 +642,11 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
|||||||
key = "theme__mode",
|
key = "theme__mode",
|
||||||
default = ThemeMode.FOLLOW_SYSTEM,
|
default = ThemeMode.FOLLOW_SYSTEM,
|
||||||
)
|
)
|
||||||
val dayThemeAdaptToApp = boolean(
|
|
||||||
key = "theme__day_theme_adapt_to_app",
|
|
||||||
default = false,
|
|
||||||
)
|
|
||||||
val dayThemeId = custom(
|
val dayThemeId = custom(
|
||||||
key = "theme__day_theme_id",
|
key = "theme__day_theme_id",
|
||||||
default = extCoreTheme("floris_day"),
|
default = extCoreTheme("floris_day"),
|
||||||
serializer = ExtensionComponentName.Serializer,
|
serializer = ExtensionComponentName.Serializer,
|
||||||
)
|
)
|
||||||
val nightThemeAdaptToApp = boolean(
|
|
||||||
key = "theme__night_theme_adapt_to_app",
|
|
||||||
default = false,
|
|
||||||
)
|
|
||||||
val nightThemeId = custom(
|
val nightThemeId = custom(
|
||||||
key = "theme__night_theme_id",
|
key = "theme__night_theme_id",
|
||||||
default = extCoreTheme("floris_night"),
|
default = extCoreTheme("floris_night"),
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package dev.patrickgold.florisboard.app
|
package dev.patrickgold.florisboard.app
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
@ -24,11 +25,11 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -41,7 +42,9 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.app.apptheme.FlorisAppTheme
|
import dev.patrickgold.florisboard.app.apptheme.FlorisAppTheme
|
||||||
|
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
|
||||||
import dev.patrickgold.florisboard.app.setup.NotificationPermissionState
|
import dev.patrickgold.florisboard.app.setup.NotificationPermissionState
|
||||||
|
import dev.patrickgold.florisboard.cacheManager
|
||||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||||
import dev.patrickgold.florisboard.lib.android.hideAppIcon
|
import dev.patrickgold.florisboard.lib.android.hideAppIcon
|
||||||
@ -70,9 +73,11 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
|
|||||||
|
|
||||||
class FlorisAppActivity : ComponentActivity() {
|
class FlorisAppActivity : ComponentActivity() {
|
||||||
private val prefs by florisPreferenceModel()
|
private val prefs by florisPreferenceModel()
|
||||||
|
private val cacheManager by cacheManager()
|
||||||
private var appTheme by mutableStateOf(AppTheme.AUTO)
|
private var appTheme by mutableStateOf(AppTheme.AUTO)
|
||||||
private var showAppIcon = true
|
private var showAppIcon = true
|
||||||
private var resourcesContext by mutableStateOf(this as Context)
|
private var resourcesContext by mutableStateOf(this as Context)
|
||||||
|
private var fileImportIntent by mutableStateOf<Intent?>(null)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
// Splash screen should be installed before calling super.onCreate()
|
// Splash screen should be installed before calling super.onCreate()
|
||||||
@ -110,14 +115,15 @@ class FlorisAppActivity : ComponentActivity() {
|
|||||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||||
setContent {
|
setContent {
|
||||||
ProvideLocalizedResources(resourcesContext) {
|
ProvideLocalizedResources(resourcesContext) {
|
||||||
FlorisAppTheme(theme = appTheme, isMaterialYouAware = prefs.advanced.useMaterialYou.observeAsState().value) {
|
val useMaterialYou by prefs.advanced.useMaterialYou.observeAsState()
|
||||||
|
FlorisAppTheme(theme = appTheme, isMaterialYouAware = useMaterialYou) {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
//SystemUiApp()
|
|
||||||
AppContent()
|
AppContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onNewIntent(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +141,21 @@ class FlorisAppActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
|
||||||
|
if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||||
|
fileImportIntent = intent
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (intent?.action == Intent.ACTION_SEND && intent.clipData != null) {
|
||||||
|
fileImportIntent = intent
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileImportIntent = null
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppContent() {
|
private fun AppContent() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
@ -167,6 +188,20 @@ class FlorisAppActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(fileImportIntent) {
|
||||||
|
val intent = fileImportIntent
|
||||||
|
if (intent != null) {
|
||||||
|
val data = if (intent.action == Intent.ACTION_VIEW) {
|
||||||
|
intent.data!!
|
||||||
|
} else {
|
||||||
|
intent.clipData!!.getItemAt(0).uri
|
||||||
|
}
|
||||||
|
val workspace = runCatching { cacheManager.readFromUriIntoCache(data) }.getOrNull()
|
||||||
|
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, workspace?.uuid))
|
||||||
|
}
|
||||||
|
fileImportIntent = null
|
||||||
|
}
|
||||||
|
|
||||||
SideEffect {
|
SideEffect {
|
||||||
navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher)
|
navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher)
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,11 @@ import dev.patrickgold.florisboard.app.devtools.DevtoolsScreen
|
|||||||
import dev.patrickgold.florisboard.app.devtools.ExportDebugLogScreen
|
import dev.patrickgold.florisboard.app.devtools.ExportDebugLogScreen
|
||||||
import dev.patrickgold.florisboard.app.ext.ExtensionEditScreen
|
import dev.patrickgold.florisboard.app.ext.ExtensionEditScreen
|
||||||
import dev.patrickgold.florisboard.app.ext.ExtensionExportScreen
|
import dev.patrickgold.florisboard.app.ext.ExtensionExportScreen
|
||||||
|
import dev.patrickgold.florisboard.app.ext.ExtensionHomeScreen
|
||||||
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreen
|
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreen
|
||||||
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
|
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
|
||||||
|
import dev.patrickgold.florisboard.app.ext.ExtensionListScreen
|
||||||
|
import dev.patrickgold.florisboard.app.ext.ExtensionListScreenType
|
||||||
import dev.patrickgold.florisboard.app.ext.ExtensionViewScreen
|
import dev.patrickgold.florisboard.app.ext.ExtensionViewScreen
|
||||||
import dev.patrickgold.florisboard.app.settings.HomeScreen
|
import dev.patrickgold.florisboard.app.settings.HomeScreen
|
||||||
import dev.patrickgold.florisboard.app.settings.about.AboutScreen
|
import dev.patrickgold.florisboard.app.settings.about.AboutScreen
|
||||||
@ -58,7 +61,7 @@ import dev.patrickgold.florisboard.app.settings.typing.TypingScreen
|
|||||||
import dev.patrickgold.florisboard.app.setup.SetupScreen
|
import dev.patrickgold.florisboard.app.setup.SetupScreen
|
||||||
import org.florisboard.lib.kotlin.curlyFormat
|
import org.florisboard.lib.kotlin.curlyFormat
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
@Suppress("FunctionName", "ConstPropertyName")
|
||||||
object Routes {
|
object Routes {
|
||||||
object Setup {
|
object Setup {
|
||||||
const val Screen = "setup"
|
const val Screen = "setup"
|
||||||
@ -117,6 +120,14 @@ object Routes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Ext {
|
object Ext {
|
||||||
|
const val Home = "ext"
|
||||||
|
|
||||||
|
const val List = "ext/list/{type}?showUpdate={showUpdate}"
|
||||||
|
fun List(
|
||||||
|
type: ExtensionListScreenType,
|
||||||
|
showUpdate: Boolean
|
||||||
|
) = List.curlyFormat("type" to type.id, "showUpdate" to showUpdate)
|
||||||
|
|
||||||
const val Edit = "ext/edit/{id}?create={serial_type}"
|
const val Edit = "ext/edit/{id}?create={serial_type}"
|
||||||
fun Edit(id: String, serialType: String? = null): String {
|
fun Edit(id: String, serialType: String? = null): String {
|
||||||
return Edit.curlyFormat("id" to id, "serial_type" to (serialType ?: ""))
|
return Edit.curlyFormat("id" to id, "serial_type" to (serialType ?: ""))
|
||||||
@ -209,12 +220,20 @@ object Routes {
|
|||||||
}
|
}
|
||||||
composable(Devtools.ExportDebugLog) { ExportDebugLogScreen() }
|
composable(Devtools.ExportDebugLog) { ExportDebugLogScreen() }
|
||||||
|
|
||||||
|
composable(Ext.Home) { ExtensionHomeScreen() }
|
||||||
|
composable(Ext.List) { navBackStack ->
|
||||||
|
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
|
||||||
|
ExtensionListScreenType.entries.firstOrNull { it.id == typeId }
|
||||||
|
} ?: error("unknown type")
|
||||||
|
val showUpdate = navBackStack.arguments?.getString("showUpdate")
|
||||||
|
ExtensionListScreen(type, showUpdate == "true")
|
||||||
|
}
|
||||||
composable(Ext.Edit) { navBackStack ->
|
composable(Ext.Edit) { navBackStack ->
|
||||||
val extensionId = navBackStack.arguments?.getString("id")
|
val extensionId = navBackStack.arguments?.getString("id")
|
||||||
val serialType = navBackStack.arguments?.getString("serial_type")
|
val serialType = navBackStack.arguments?.getString("serial_type")
|
||||||
ExtensionEditScreen(
|
ExtensionEditScreen(
|
||||||
id = extensionId.toString(),
|
id = extensionId.toString(),
|
||||||
createSerialType = serialType.takeIf { it != null && it.isNotBlank() },
|
createSerialType = serialType.takeIf { !it.isNullOrBlank() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Ext.Export) { navBackStack ->
|
composable(Ext.Export) { navBackStack ->
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
package dev.patrickgold.florisboard.app.ext
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Shop
|
||||||
|
import androidx.compose.material.icons.outlined.FileDownload
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.patrickgold.florisboard.R
|
||||||
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
|
import dev.patrickgold.florisboard.lib.android.launchUrl
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.Extension
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.generateUpdateUrl
|
||||||
|
import org.florisboard.lib.kotlin.curlyFormat
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UpdateBox(extensionIndex: List<Extension>) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
FlorisOutlinedBox(
|
||||||
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 4.dp),
|
||||||
|
text = stringRes(id = R.string.ext__update_box__internet_permission_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 6.dp),
|
||||||
|
) {
|
||||||
|
FlorisTextButton(
|
||||||
|
onClick = {
|
||||||
|
context.launchUrl(extensionIndex.generateUpdateUrl(version = "v~draft2", host = "fladdonstest.patrickgold.dev"))
|
||||||
|
},
|
||||||
|
icon = Icons.Outlined.FileDownload,
|
||||||
|
text = stringRes(id = R.string.ext__update_box__search_for_updates)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddonManagementReferenceBox(
|
||||||
|
type: ExtensionListScreenType
|
||||||
|
) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
FlorisOutlinedBox(
|
||||||
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
|
title = stringRes(id = R.string.ext__addon_management_box__managing_placeholder).curlyFormat(
|
||||||
|
"extensions" to type.let { stringRes(id = it.titleResId).lowercase() }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
text = stringRes(id = R.string.ext__addon_management_box__addon_manager_info),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 6.dp),
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
FlorisTextButton(
|
||||||
|
onClick = {
|
||||||
|
val route = Routes.Ext.List(type, showUpdate = true)
|
||||||
|
navController.navigate(
|
||||||
|
route
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon = Icons.Default.Shop,
|
||||||
|
text = stringRes(id = R.string.ext__addon_management_box__go_to_page).curlyFormat(
|
||||||
|
"ext_home_title" to stringRes(type.titleResId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package dev.patrickgold.florisboard.app.ext
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Input
|
||||||
|
import androidx.compose.material.icons.filled.Keyboard
|
||||||
|
import androidx.compose.material.icons.filled.Language
|
||||||
|
import androidx.compose.material.icons.filled.Palette
|
||||||
|
import androidx.compose.material.icons.filled.Shop
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.patrickgold.florisboard.BuildConfig
|
||||||
|
import dev.patrickgold.florisboard.R
|
||||||
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
|
import dev.patrickgold.florisboard.lib.android.launchUrl
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
|
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||||
|
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtensionHomeScreen() = FlorisScreen {
|
||||||
|
title = stringRes(R.string.ext__home__title)
|
||||||
|
previewFieldVisible = false
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val extensionManager by context.extensionManager()
|
||||||
|
val extensionIndex = extensionManager.combinedExtensionList()
|
||||||
|
|
||||||
|
content {
|
||||||
|
FlorisOutlinedBox(
|
||||||
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 4.dp),
|
||||||
|
text = stringRes(id = R.string.ext__home__info),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 6.dp),
|
||||||
|
) {
|
||||||
|
FlorisTextButton(
|
||||||
|
onClick = {
|
||||||
|
context.launchUrl("https://${BuildConfig.FLADDONS_STORE_URL}/")
|
||||||
|
},
|
||||||
|
icon = Icons.Default.Shop,
|
||||||
|
text = stringRes(id = R.string.ext__home__visit_store),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
FlorisTextButton(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, null))
|
||||||
|
},
|
||||||
|
icon = Icons.AutoMirrored.Filled.Input,
|
||||||
|
text = stringRes(R.string.action__import),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBox(extensionIndex = extensionIndex)
|
||||||
|
|
||||||
|
PreferenceGroup(title = stringRes(id = R.string.ext__home__visit_store)) {
|
||||||
|
Preference(
|
||||||
|
icon = Icons.Default.Palette,
|
||||||
|
title = stringRes(R.string.ext__list__ext_theme),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_THEME,false))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Preference(
|
||||||
|
icon = Icons.Default.Keyboard,
|
||||||
|
title = stringRes(R.string.ext__list__ext_keyboard),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_KEYBOARD,false))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Preference(
|
||||||
|
icon = Icons.Default.Language,
|
||||||
|
title = stringRes(R.string.ext__list__ext_languagepack),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_LANGUAGEPACK,false))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -100,12 +99,6 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
|||||||
val cacheManager by context.cacheManager()
|
val cacheManager by context.cacheManager()
|
||||||
val extensionManager by context.extensionManager()
|
val extensionManager by context.extensionManager()
|
||||||
|
|
||||||
val initWsUuid by rememberSaveable { mutableStateOf(initUuid) }
|
|
||||||
var importResult by remember {
|
|
||||||
val workspace = initWsUuid?.let { cacheManager.importer.getWorkspaceByUuid(it) }?.let { resultOk(it) }
|
|
||||||
mutableStateOf(workspace)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSkipReason(fileInfo: CacheManager.FileInfo): Int {
|
fun getSkipReason(fileInfo: CacheManager.FileInfo): Int {
|
||||||
return when {
|
return when {
|
||||||
!FileRegistry.matchesFileFilter(fileInfo, type.supportedFiles) -> {
|
!FileRegistry.matchesFileFilter(fileInfo, type.supportedFiles) -> {
|
||||||
@ -119,29 +112,37 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
|||||||
NATIVE_NULLPTR.toInt()
|
NATIVE_NULLPTR.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileInfo.mediaType == FileRegistry.FlexExtension.mediaType -> {
|
else -> { // ext == null
|
||||||
R.string.ext__import__file_skip_ext_corrupted
|
R.string.ext__import__file_skip_ext_corrupted
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
NATIVE_NULLPTR.toInt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Result<CacheManager.ImporterWorkspace>.mapSkipReasons(): Result<CacheManager.ImporterWorkspace> {
|
||||||
|
return this.map { workspace ->
|
||||||
|
workspace.inputFileInfos.forEach { fileInfo ->
|
||||||
|
fileInfo.skipReason = getSkipReason(fileInfo)
|
||||||
|
}
|
||||||
|
workspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var importResult by remember(initUuid) {
|
||||||
|
val workspace = initUuid?.let { cacheManager.importer.getWorkspaceByUuid(it) }
|
||||||
|
?.let { resultOk(it) }
|
||||||
|
?.mapSkipReasons()
|
||||||
|
mutableStateOf(workspace)
|
||||||
|
}
|
||||||
|
|
||||||
val importLauncher = rememberLauncherForActivityResult(
|
val importLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetMultipleContents(),
|
contract = ActivityResultContracts.GetMultipleContents(),
|
||||||
onResult = { uriList ->
|
onResult = { uriList ->
|
||||||
// If uri is null it indicates that the selection activity
|
// If uri is null it indicates that the selection activity
|
||||||
// was cancelled (mostly by pressing the back button), so
|
// was cancelled (mostly by pressing the back button), so
|
||||||
// we don't display an error message here.
|
// we don't display an error message here.
|
||||||
if (uriList.isNullOrEmpty()) return@rememberLauncherForActivityResult
|
if (uriList.isEmpty()) return@rememberLauncherForActivityResult
|
||||||
importResult?.getOrNull()?.close()
|
importResult?.getOrNull()?.close()
|
||||||
importResult = runCatching { cacheManager.readFromUriIntoCache(uriList) }.map { workspace ->
|
importResult = runCatching { cacheManager.readFromUriIntoCache(uriList) }.mapSkipReasons()
|
||||||
workspace.inputFileInfos.forEach { fileInfo ->
|
|
||||||
fileInfo.skipReason = getSkipReason(fileInfo)
|
|
||||||
}
|
|
||||||
workspace
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -197,15 +198,17 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
content {
|
content {
|
||||||
FlorisOutlinedButton(
|
if (initUuid == null) {
|
||||||
onClick = {
|
FlorisOutlinedButton(
|
||||||
importLauncher.launch("*/*")
|
onClick = {
|
||||||
},
|
importLauncher.launch("*/*")
|
||||||
modifier = Modifier
|
},
|
||||||
.padding(vertical = 16.dp)
|
modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally),
|
.padding(vertical = 16.dp)
|
||||||
text = stringRes(R.string.action__select_files),
|
.align(Alignment.CenterHorizontally),
|
||||||
)
|
text = stringRes(R.string.action__select_files),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val result = importResult
|
val result = importResult
|
||||||
when {
|
when {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 Patrick Goldinger
|
* Copyright (C) 2024 Patrick Goldinger
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -16,84 +16,135 @@
|
|||||||
|
|
||||||
package dev.patrickgold.florisboard.app.ext
|
package dev.patrickgold.florisboard.app.ext
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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 androidx.navigation.NavController
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
|
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.ExtensionManager
|
||||||
|
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
||||||
|
|
||||||
|
enum class ExtensionListScreenType(
|
||||||
|
val id: String,
|
||||||
|
@StringRes val titleResId: Int,
|
||||||
|
val getExtensionIndex: (ExtensionManager) -> ExtensionManager.ExtensionIndex<*>,
|
||||||
|
val launchExtensionCreate: ((NavController) -> Unit)?,
|
||||||
|
) {
|
||||||
|
EXT_THEME(
|
||||||
|
id = "ext-theme",
|
||||||
|
titleResId = R.string.ext__list__ext_theme,
|
||||||
|
getExtensionIndex = { it.themes },
|
||||||
|
launchExtensionCreate = { it.navigate(Routes.Ext.Edit("null", ThemeExtension.SERIAL_TYPE)) },
|
||||||
|
),
|
||||||
|
EXT_KEYBOARD(
|
||||||
|
id = "ext-keyboard",
|
||||||
|
titleResId = R.string.ext__list__ext_keyboard,
|
||||||
|
getExtensionIndex = { it.keyboardExtensions },
|
||||||
|
launchExtensionCreate = null,//{ it.navigate(Routes.Ext.Edit("null", KeyboardExtension.SERIAL_TYPE)) },
|
||||||
|
),
|
||||||
|
EXT_LANGUAGEPACK(
|
||||||
|
id = "ext-languagepack",
|
||||||
|
titleResId = R.string.ext__list__ext_languagepack,
|
||||||
|
getExtensionIndex = { it.languagePacks },
|
||||||
|
launchExtensionCreate = null,//{ it.navigate(Routes.Ext.Edit("null", LanguagePackExtension.SERIAL_TYPE)) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionListScreen() = FlorisScreen {
|
fun ExtensionListScreen(type: ExtensionListScreenType, showUpdate: Boolean) = FlorisScreen {
|
||||||
title = stringRes(R.string.about__title)
|
title = stringRes(type.titleResId)
|
||||||
|
previewFieldVisible = false
|
||||||
|
|
||||||
/*val navController = LocalNavController.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val extensionManager = ExtensionManager.def
|
val navController = LocalNavController.current
|
||||||
|
val extensionManager by context.extensionManager()
|
||||||
|
val extensionIndex by type.getExtensionIndex(extensionManager).observeAsNonNullState()
|
||||||
|
|
||||||
Column(
|
content {
|
||||||
verticalArrangement = Arrangement.Top,
|
if (showUpdate) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
UpdateBox(extensionIndex = extensionIndex)
|
||||||
modifier = Modifier
|
}
|
||||||
.fillMaxWidth()
|
for (ext in extensionIndex) {
|
||||||
.padding(top = 24.dp, bottom = 32.dp)
|
FlorisOutlinedBox(
|
||||||
) {
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
FlorisAppIcon()
|
title = ext.meta.title,
|
||||||
Text(
|
subtitle = ext.meta.id,
|
||||||
text = stringRes(R.string.floris_app_name),
|
) {
|
||||||
fontSize = 24.sp,
|
Text(
|
||||||
fontWeight = FontWeight.SemiBold,
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
text = ext.meta.description ?: "",
|
||||||
)
|
style = MaterialTheme.typography.bodySmall,
|
||||||
}
|
)
|
||||||
Preference(
|
Row(
|
||||||
icon = R.drawable.ic_info,
|
modifier = Modifier
|
||||||
title = stringRes(R.string.about__version__title),
|
.fillMaxWidth()
|
||||||
summary = appVersion,
|
.padding(horizontal = 6.dp),
|
||||||
onClick = {
|
) {
|
||||||
try {
|
FlorisTextButton(
|
||||||
val isImeSelected = InputMethodUtils.checkIsFlorisboardSelected(context)
|
onClick = {
|
||||||
if (isImeSelected) {
|
navController.navigate(Routes.Ext.View(ext.meta.id))
|
||||||
FlorisClipboardManager.getInstance().addNewPlaintext(appVersion)
|
},
|
||||||
} else {
|
icon = Icons.Outlined.Info,
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
text = stringRes(id = R.string.ext__list__view_details),//stringRes(R.string.action__add),
|
||||||
val clip = ClipData.newPlainText("Florisboard version", appVersion)
|
colors = ButtonDefaults.textButtonColors(),
|
||||||
clipboard.setPrimaryClip(clip)
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
FlorisTextButton(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Routes.Ext.Edit(ext.meta.id))
|
||||||
|
},
|
||||||
|
icon = Icons.Default.Edit,
|
||||||
|
text = stringRes(R.string.action__edit),
|
||||||
|
enabled = extensionManager.canDelete(ext),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Toast.makeText(context, R.string.about__version_copied__title, Toast.LENGTH_SHORT).show()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Toast.makeText(
|
|
||||||
context, context.getString(R.string.about__version_copied__error, e.message), Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
Preference(
|
|
||||||
icon = R.drawable.ic_history,
|
if (type.launchExtensionCreate != null) {
|
||||||
title = stringRes(R.string.about__changelog__title),
|
floatingActionButton {
|
||||||
summary = stringRes(R.string.about__changelog__summary),
|
ExtendedFloatingActionButton(
|
||||||
onClick = { launchUrl(context, R.string.florisboard__changelog_url, arrayOf(BuildConfig.VERSION_NAME)) },
|
icon = {
|
||||||
)
|
Icon(
|
||||||
Preference(
|
imageVector = Icons.Default.Add,
|
||||||
icon = R.drawable.ic_code,
|
contentDescription = stringRes(id = R.string.ext__editor__title_create_any),
|
||||||
title = stringRes(R.string.about__repository__title),
|
)
|
||||||
summary = stringRes(R.string.about__repository__summary),
|
},
|
||||||
onClick = { launchUrl(context, R.string.florisboard__repo_url) },
|
text = {
|
||||||
)
|
Text(
|
||||||
Preference(
|
text = stringRes(id = R.string.ext__editor__title_create_any),
|
||||||
icon = R.drawable.ic_policy,
|
)
|
||||||
title = stringRes(R.string.about__privacy_policy__title),
|
},
|
||||||
summary = stringRes(R.string.about__privacy_policy__summary),
|
shape = FloatingActionButtonDefaults.extendedFabShape,
|
||||||
onClick = { launchUrl(context, R.string.florisboard__privacy_policy_url) },
|
onClick = { type.launchExtensionCreate.invoke(navController) },
|
||||||
)
|
)
|
||||||
Preference(
|
}
|
||||||
icon = R.drawable.ic_description,
|
}
|
||||||
title = stringRes(R.string.about__project_license__title),
|
|
||||||
summary = stringRes(R.string.about__project_license__summary, "license_name" to "Apache 2.0"),
|
|
||||||
onClick = { navController.navigate(Routes.Settings.ProjectLicense) },
|
|
||||||
)
|
|
||||||
Preference(
|
|
||||||
icon = R.drawable.ic_description,
|
|
||||||
title = stringRes(id = R.string.about__third_party_licenses__title),
|
|
||||||
summary = stringRes(id = R.string.about__third_party_licenses__summary),
|
|
||||||
onClick = { navController.navigate(Routes.Settings.ThirdPartyLicenses) },
|
|
||||||
)*/
|
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,8 @@ package dev.patrickgold.florisboard.app.ext
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Link
|
import androidx.compose.material.icons.filled.Link
|
||||||
import androidx.compose.material.icons.filled.Mail
|
|
||||||
import androidx.compose.material.icons.outlined.Mail
|
import androidx.compose.material.icons.outlined.Mail
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -32,13 +30,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.patrickgold.florisboard.R
|
|
||||||
import dev.patrickgold.florisboard.lib.android.launchUrl
|
import dev.patrickgold.florisboard.lib.android.launchUrl
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisChip
|
import dev.patrickgold.florisboard.lib.compose.FlorisChip
|
||||||
import dev.patrickgold.florisboard.lib.ext.ExtensionMaintainer
|
import dev.patrickgold.florisboard.lib.ext.ExtensionMaintainer
|
||||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionMaintainerChip(
|
fun ExtensionMaintainerChip(
|
||||||
maintainer: ExtensionMaintainer,
|
maintainer: ExtensionMaintainer,
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2021 Patrick Goldinger
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.patrickgold.florisboard.app.ext
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
|
|
||||||
class ImportFileActivity : ComponentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val data = intent.data
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,14 +18,13 @@ package dev.patrickgold.florisboard.app.settings
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Adb
|
import androidx.compose.material.icons.automirrored.outlined.Assignment
|
||||||
|
import androidx.compose.material.icons.filled.Extension
|
||||||
import androidx.compose.material.icons.filled.Gesture
|
import androidx.compose.material.icons.filled.Gesture
|
||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material.icons.filled.Language
|
||||||
import androidx.compose.material.icons.filled.LibraryBooks
|
|
||||||
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
|
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
|
||||||
import androidx.compose.material.icons.filled.SmartButton
|
import androidx.compose.material.icons.filled.SmartButton
|
||||||
import androidx.compose.material.icons.filled.Spellcheck
|
import androidx.compose.material.icons.filled.Spellcheck
|
||||||
import androidx.compose.material.icons.outlined.Assignment
|
|
||||||
import androidx.compose.material.icons.outlined.Build
|
import androidx.compose.material.icons.outlined.Build
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.Keyboard
|
import androidx.compose.material.icons.outlined.Keyboard
|
||||||
@ -131,18 +130,13 @@ fun HomeScreen() = FlorisScreen {
|
|||||||
title = stringRes(R.string.settings__typing__title),
|
title = stringRes(R.string.settings__typing__title),
|
||||||
onClick = { navController.navigate(Routes.Settings.Typing) },
|
onClick = { navController.navigate(Routes.Settings.Typing) },
|
||||||
)
|
)
|
||||||
Preference(
|
|
||||||
icon = Icons.Default.LibraryBooks,
|
|
||||||
title = stringRes(R.string.settings__dictionary__title),
|
|
||||||
onClick = { navController.navigate(Routes.Settings.Dictionary) },
|
|
||||||
)
|
|
||||||
Preference(
|
Preference(
|
||||||
icon = Icons.Default.Gesture,
|
icon = Icons.Default.Gesture,
|
||||||
title = stringRes(R.string.settings__gestures__title),
|
title = stringRes(R.string.settings__gestures__title),
|
||||||
onClick = { navController.navigate(Routes.Settings.Gestures) },
|
onClick = { navController.navigate(Routes.Settings.Gestures) },
|
||||||
)
|
)
|
||||||
Preference(
|
Preference(
|
||||||
icon = Icons.Outlined.Assignment,
|
icon = Icons.AutoMirrored.Outlined.Assignment,
|
||||||
title = stringRes(R.string.settings__clipboard__title),
|
title = stringRes(R.string.settings__clipboard__title),
|
||||||
onClick = { navController.navigate(Routes.Settings.Clipboard) },
|
onClick = { navController.navigate(Routes.Settings.Clipboard) },
|
||||||
)
|
)
|
||||||
@ -152,9 +146,9 @@ fun HomeScreen() = FlorisScreen {
|
|||||||
onClick = { navController.navigate(Routes.Settings.Media) },
|
onClick = { navController.navigate(Routes.Settings.Media) },
|
||||||
)
|
)
|
||||||
Preference(
|
Preference(
|
||||||
icon = Icons.Default.Adb,
|
icon = Icons.Default.Extension,
|
||||||
title = stringRes(R.string.devtools__title),
|
title = stringRes(R.string.ext__home__title),
|
||||||
onClick = { navController.navigate(Routes.Devtools.Home) },
|
onClick = { navController.navigate(Routes.Ext.Home) },
|
||||||
)
|
)
|
||||||
Preference(
|
Preference(
|
||||||
icon = Icons.Outlined.Build,
|
icon = Icons.Outlined.Build,
|
||||||
|
@ -28,12 +28,12 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.assetManager
|
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||||
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
|
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
|
||||||
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
|
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
|
||||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
||||||
|
import dev.patrickgold.florisboard.lib.io.loadTextAsset
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProjectLicenseScreen() = FlorisScreen {
|
fun ProjectLicenseScreen() = FlorisScreen {
|
||||||
@ -41,7 +41,6 @@ fun ProjectLicenseScreen() = FlorisScreen {
|
|||||||
scrollable = false
|
scrollable = false
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val assetManager by context.assetManager()
|
|
||||||
|
|
||||||
content {
|
content {
|
||||||
// Forcing LTR because the Apache 2.0 License shipped and displayed
|
// Forcing LTR because the Apache 2.0 License shipped and displayed
|
||||||
@ -54,8 +53,8 @@ fun ProjectLicenseScreen() = FlorisScreen {
|
|||||||
.florisVerticalScroll()
|
.florisVerticalScroll()
|
||||||
.florisHorizontalScroll(),
|
.florisHorizontalScroll(),
|
||||||
) {
|
) {
|
||||||
val licenseText = assetManager.loadTextAsset(
|
val licenseText = FlorisRef.assets("license/project_license.txt").loadTextAsset(
|
||||||
FlorisRef.assets("license/project_license.txt")
|
context
|
||||||
).getOrElse {
|
).getOrElse {
|
||||||
stringRes(R.string.about__project_license__error_license_text_failed, "error_message" to (it.message ?: ""))
|
stringRes(R.string.about__project_license__error_license_text_failed, "error_message" to (it.message ?: ""))
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package dev.patrickgold.florisboard.app.settings.advanced
|
package dev.patrickgold.florisboard.app.settings.advanced
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Adb
|
||||||
import androidx.compose.material.icons.filled.Archive
|
import androidx.compose.material.icons.filled.Archive
|
||||||
import androidx.compose.material.icons.filled.FormatPaint
|
import androidx.compose.material.icons.filled.FormatPaint
|
||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material.icons.filled.Language
|
||||||
@ -166,6 +167,11 @@ fun AdvancedScreen() = FlorisScreen {
|
|||||||
title = stringRes(R.string.pref__advanced__incognito_mode__label),
|
title = stringRes(R.string.pref__advanced__incognito_mode__label),
|
||||||
entries = IncognitoMode.listEntries(),
|
entries = IncognitoMode.listEntries(),
|
||||||
)
|
)
|
||||||
|
Preference(
|
||||||
|
icon = Icons.Default.Adb,
|
||||||
|
title = stringRes(R.string.devtools__title),
|
||||||
|
onClick = { navController.navigate(Routes.Devtools.Home) },
|
||||||
|
)
|
||||||
|
|
||||||
PreferenceGroup(title = stringRes(R.string.backup_and_restore__title)) {
|
PreferenceGroup(title = stringRes(R.string.backup_and_restore__title)) {
|
||||||
Preference(
|
Preference(
|
||||||
|
@ -16,83 +16,56 @@
|
|||||||
|
|
||||||
package dev.patrickgold.florisboard.app.settings.theme
|
package dev.patrickgold.florisboard.app.settings.theme
|
||||||
|
|
||||||
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.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.DarkMode
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material.icons.filled.Input
|
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
import androidx.compose.material.icons.filled.LightMode
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.app.LocalNavController
|
|
||||||
import dev.patrickgold.florisboard.app.Routes
|
|
||||||
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
|
|
||||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||||
import dev.patrickgold.florisboard.extensionManager
|
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.ThemeExtensionComponent
|
||||||
import dev.patrickgold.florisboard.lib.android.showLongToast
|
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisConfirmDeleteDialog
|
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
|
|
||||||
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||||
import dev.patrickgold.florisboard.lib.compose.rippleClickable
|
import dev.patrickgold.florisboard.lib.compose.rippleClickable
|
||||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
import dev.patrickgold.florisboard.lib.ext.Extension
|
|
||||||
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
||||||
import dev.patrickgold.florisboard.themeManager
|
import dev.patrickgold.florisboard.themeManager
|
||||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||||
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
|
||||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
|
||||||
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
|
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
|
||||||
|
|
||||||
enum class ThemeManagerScreenAction(val id: String) {
|
enum class ThemeManagerScreenAction(val id: String) {
|
||||||
SELECT_DAY("select-day"),
|
SELECT_DAY("select-day"),
|
||||||
SELECT_NIGHT("select-night"),
|
SELECT_NIGHT("select-night");
|
||||||
MANAGE("manage-installed-themes");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalJetPrefDatastoreUi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||||
title = stringRes(when (action) {
|
title = stringRes(when (action) {
|
||||||
ThemeManagerScreenAction.SELECT_DAY -> R.string.settings__theme_manager__title_day
|
ThemeManagerScreenAction.SELECT_DAY -> R.string.settings__theme_manager__title_day
|
||||||
ThemeManagerScreenAction.SELECT_NIGHT -> R.string.settings__theme_manager__title_night
|
ThemeManagerScreenAction.SELECT_NIGHT -> R.string.settings__theme_manager__title_night
|
||||||
ThemeManagerScreenAction.MANAGE -> R.string.settings__theme_manager__title_manage
|
|
||||||
else -> error("Theme manager screen action must not be null")
|
else -> error("Theme manager screen action must not be null")
|
||||||
})
|
})
|
||||||
previewFieldVisible = action != ThemeManagerScreenAction.MANAGE
|
previewFieldVisible = true
|
||||||
|
|
||||||
val prefs by florisPreferenceModel()
|
val prefs by florisPreferenceModel()
|
||||||
val navController = LocalNavController.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val extensionManager by context.extensionManager()
|
val extensionManager by context.extensionManager()
|
||||||
val themeManager by context.themeManager()
|
val themeManager by context.themeManager()
|
||||||
|
|
||||||
val indexedThemeExtensions by extensionManager.themes.observeAsNonNullState()
|
val indexedThemeExtensions by extensionManager.themes.observeAsNonNullState()
|
||||||
val selectedManagerThemeId = remember { mutableStateOf<ExtensionComponentName?>(null) }
|
|
||||||
val extGroupedThemes = remember(indexedThemeExtensions) {
|
val extGroupedThemes = remember(indexedThemeExtensions) {
|
||||||
buildMap<String, List<ThemeExtensionComponent>> {
|
buildMap<String, List<ThemeExtensionComponent>> {
|
||||||
for (ext in indexedThemeExtensions) {
|
for (ext in indexedThemeExtensions) {
|
||||||
@ -104,7 +77,6 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
|||||||
fun getThemeIdPref() = when (action) {
|
fun getThemeIdPref() = when (action) {
|
||||||
ThemeManagerScreenAction.SELECT_DAY -> prefs.theme.dayThemeId
|
ThemeManagerScreenAction.SELECT_DAY -> prefs.theme.dayThemeId
|
||||||
ThemeManagerScreenAction.SELECT_NIGHT -> prefs.theme.nightThemeId
|
ThemeManagerScreenAction.SELECT_NIGHT -> prefs.theme.nightThemeId
|
||||||
ThemeManagerScreenAction.MANAGE -> error("internal error in manager logic")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTheme(extId: String, componentId: String) {
|
fun setTheme(extId: String, componentId: String) {
|
||||||
@ -114,18 +86,13 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
|||||||
ThemeManagerScreenAction.SELECT_NIGHT -> {
|
ThemeManagerScreenAction.SELECT_NIGHT -> {
|
||||||
getThemeIdPref().set(extComponentName)
|
getThemeIdPref().set(extComponentName)
|
||||||
}
|
}
|
||||||
ThemeManagerScreenAction.MANAGE -> {
|
|
||||||
selectedManagerThemeId.value = extComponentName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeThemeId by when (action) {
|
val activeThemeId by when (action) {
|
||||||
ThemeManagerScreenAction.SELECT_DAY,
|
ThemeManagerScreenAction.SELECT_DAY,
|
||||||
ThemeManagerScreenAction.SELECT_NIGHT -> getThemeIdPref().observeAsState()
|
ThemeManagerScreenAction.SELECT_NIGHT -> getThemeIdPref().observeAsState()
|
||||||
ThemeManagerScreenAction.MANAGE -> selectedManagerThemeId
|
|
||||||
}
|
}
|
||||||
var themeExtToDelete by remember { mutableStateOf<Extension?>(null) }
|
|
||||||
|
|
||||||
content {
|
content {
|
||||||
DisposableEffect(activeThemeId) {
|
DisposableEffect(activeThemeId) {
|
||||||
@ -135,34 +102,12 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val grayColor = LocalContentColor.current.copy(alpha = 0.56f)
|
val grayColor = LocalContentColor.current.copy(alpha = 0.56f)
|
||||||
if (action == ThemeManagerScreenAction.MANAGE) {
|
|
||||||
FlorisOutlinedBox(
|
|
||||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
|
||||||
) {
|
|
||||||
this@content.Preference(
|
|
||||||
onClick = { navController.navigate(
|
|
||||||
Routes.Ext.Edit("null", ThemeExtension.SERIAL_TYPE)
|
|
||||||
) },
|
|
||||||
icon = Icons.Default.Add,
|
|
||||||
title = stringRes(R.string.ext__editor__title_create_theme),
|
|
||||||
)
|
|
||||||
this@content.Preference(
|
|
||||||
onClick = { navController.navigate(
|
|
||||||
Routes.Ext.Import(ExtensionImportScreenType.EXT_THEME, null)
|
|
||||||
) },
|
|
||||||
icon = Icons.Default.Input,
|
|
||||||
title = stringRes(R.string.action__import),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for ((extensionId, configs) in extGroupedThemes) key(extensionId) {
|
for ((extensionId, configs) in extGroupedThemes) key(extensionId) {
|
||||||
val ext = extensionManager.getExtensionById(extensionId)!!
|
val ext = extensionManager.getExtensionById(extensionId)!!
|
||||||
FlorisOutlinedBox(
|
FlorisOutlinedBox(
|
||||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
title = ext.meta.title,
|
title = ext.meta.title,
|
||||||
onTitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
|
||||||
subtitle = extensionId,
|
subtitle = extensionId,
|
||||||
onSubtitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
|
||||||
) {
|
) {
|
||||||
for (config in configs) key(extensionId, config.id) {
|
for (config in configs) key(extensionId, config.id) {
|
||||||
JetPrefListItem(
|
JetPrefListItem(
|
||||||
@ -171,8 +116,8 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
|||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = activeThemeId?.extensionId == extensionId &&
|
selected = activeThemeId.extensionId == extensionId &&
|
||||||
activeThemeId?.componentId == config.id,
|
activeThemeId.componentId == config.id,
|
||||||
onClick = null,
|
onClick = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -191,51 +136,7 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (action == ThemeManagerScreenAction.MANAGE && extensionManager.canDelete(ext)) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 6.dp),
|
|
||||||
) {
|
|
||||||
FlorisTextButton(
|
|
||||||
onClick = {
|
|
||||||
themeExtToDelete = ext
|
|
||||||
},
|
|
||||||
icon = Icons.Default.Delete,
|
|
||||||
text = stringRes(R.string.action__delete),
|
|
||||||
colors = ButtonDefaults.textButtonColors(
|
|
||||||
contentColor = MaterialTheme.colorScheme.error,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
FlorisTextButton(
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(Routes.Ext.Edit(ext.meta.id))
|
|
||||||
},
|
|
||||||
icon = Icons.Default.Edit,
|
|
||||||
text = stringRes(R.string.action__edit),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeExtToDelete != null) {
|
|
||||||
FlorisConfirmDeleteDialog(
|
|
||||||
onConfirm = {
|
|
||||||
runCatching {
|
|
||||||
extensionManager.delete(themeExtToDelete!!)
|
|
||||||
}.onFailure { error ->
|
|
||||||
context.showLongToast(
|
|
||||||
R.string.error__snackbar_message,
|
|
||||||
"error_message" to error.localizedMessage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
themeExtToDelete = null
|
|
||||||
},
|
|
||||||
onDismiss = { themeExtToDelete = null },
|
|
||||||
what = themeExtToDelete!!.meta.title,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,35 +16,32 @@
|
|||||||
|
|
||||||
package dev.patrickgold.florisboard.app.settings.theme
|
package dev.patrickgold.florisboard.app.settings.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BrightnessAuto
|
import androidx.compose.material.icons.filled.BrightnessAuto
|
||||||
import androidx.compose.material.icons.filled.DarkMode
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
import androidx.compose.material.icons.filled.FormatPaint
|
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
import androidx.compose.material.icons.filled.LightMode
|
||||||
import androidx.compose.material.icons.outlined.Palette
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.app.LocalNavController
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
import dev.patrickgold.florisboard.app.Routes
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
|
import dev.patrickgold.florisboard.app.ext.AddonManagementReferenceBox
|
||||||
|
import dev.patrickgold.florisboard.app.ext.ExtensionListScreenType
|
||||||
|
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||||
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
||||||
import dev.patrickgold.florisboard.lib.android.launchUrl
|
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard
|
import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
|
import dev.patrickgold.florisboard.themeManager
|
||||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
|
||||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ThemeScreen() = FlorisScreen {
|
fun ThemeScreen() = FlorisScreen {
|
||||||
@ -53,13 +50,21 @@ fun ThemeScreen() = FlorisScreen {
|
|||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
val themeManager by context.themeManager()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ThemeManager.getThemeLabel(id: ExtensionComponentName): String {
|
||||||
|
val configs by indexedThemeConfigs.observeAsState()
|
||||||
|
configs?.get(id)?.let { return it.label }
|
||||||
|
return id.toString()
|
||||||
|
}
|
||||||
|
|
||||||
content {
|
content {
|
||||||
val themeMode by prefs.theme.mode.observeAsState()
|
val themeMode by prefs.theme.mode.observeAsState()
|
||||||
val dayThemeId by prefs.theme.dayThemeId.observeAsState()
|
val dayThemeId by prefs.theme.dayThemeId.observeAsState()
|
||||||
val nightThemeId by prefs.theme.nightThemeId.observeAsState()
|
val nightThemeId by prefs.theme.nightThemeId.observeAsState()
|
||||||
|
|
||||||
Card(modifier = Modifier.padding(8.dp)) {
|
/*Card(modifier = Modifier.padding(8.dp)) {
|
||||||
Column(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")
|
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 = {
|
Button(onClick = {
|
||||||
@ -68,7 +73,7 @@ fun ThemeScreen() = FlorisScreen {
|
|||||||
Text("Open Feedback Thread")
|
Text("Open Feedback Thread")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
ListPreference(
|
ListPreference(
|
||||||
prefs.theme.mode,
|
prefs.theme.mode,
|
||||||
@ -85,53 +90,24 @@ fun ThemeScreen() = FlorisScreen {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Preference(
|
Preference(
|
||||||
icon = Icons.Outlined.Palette,
|
icon = Icons.Default.LightMode,
|
||||||
title = stringRes(R.string.settings__theme_manager__title_manage),
|
title = stringRes(R.string.pref__theme__day),
|
||||||
|
summary = themeManager.getThemeLabel(dayThemeId),
|
||||||
|
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_NIGHT },
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.MANAGE))
|
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_DAY))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Preference(
|
||||||
|
icon = Icons.Default.DarkMode,
|
||||||
|
title = stringRes(R.string.pref__theme__night),
|
||||||
|
summary = themeManager.getThemeLabel(nightThemeId),
|
||||||
|
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_DAY },
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_NIGHT))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
PreferenceGroup(
|
AddonManagementReferenceBox(type = ExtensionListScreenType.EXT_THEME)
|
||||||
title = stringRes(R.string.pref__theme__day),
|
|
||||||
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_NIGHT },
|
|
||||||
) {
|
|
||||||
Preference(
|
|
||||||
icon = Icons.Default.LightMode,
|
|
||||||
title = stringRes(R.string.pref__theme__any_theme__label),
|
|
||||||
summary = dayThemeId.toString(),
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_DAY))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
SwitchPreference(
|
|
||||||
prefs.theme.dayThemeAdaptToApp,
|
|
||||||
icon = Icons.Default.FormatPaint,
|
|
||||||
title = stringRes(R.string.pref__theme__any_theme_adapt_to_app__label),
|
|
||||||
summary = stringRes(R.string.pref__theme__any_theme_adapt_to_app__summary),
|
|
||||||
visibleIf = { false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferenceGroup(
|
|
||||||
title = stringRes(R.string.pref__theme__night),
|
|
||||||
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_DAY },
|
|
||||||
) {
|
|
||||||
Preference(
|
|
||||||
icon = Icons.Default.DarkMode,
|
|
||||||
title = stringRes(R.string.pref__theme__any_theme__label),
|
|
||||||
summary = nightThemeId.toString(),
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_NIGHT))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
SwitchPreference(
|
|
||||||
prefs.theme.nightThemeAdaptToApp,
|
|
||||||
icon = Icons.Default.FormatPaint,
|
|
||||||
title = stringRes(R.string.pref__theme__any_theme_adapt_to_app__label),
|
|
||||||
summary = stringRes(R.string.pref__theme__any_theme_adapt_to_app__summary),
|
|
||||||
visibleIf = { false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,9 @@ package dev.patrickgold.florisboard.app.settings.typing
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||||
import androidx.compose.material.icons.filled.Contacts
|
import androidx.compose.material.icons.filled.Contacts
|
||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material.icons.filled.Language
|
||||||
import androidx.compose.material.icons.filled.LibraryBooks
|
|
||||||
import androidx.compose.material.icons.filled.SpaceBar
|
import androidx.compose.material.icons.filled.SpaceBar
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -32,6 +32,8 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
|
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
|
||||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
|
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
|
||||||
@ -42,6 +44,7 @@ import dev.patrickgold.jetpref.datastore.model.observeAsState
|
|||||||
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
|
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
|
||||||
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
||||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
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.PreferenceGroup
|
||||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||||
|
|
||||||
@ -51,6 +54,8 @@ fun TypingScreen() = FlorisScreen {
|
|||||||
title = stringRes(R.string.settings__typing__title)
|
title = stringRes(R.string.settings__typing__title)
|
||||||
previewFieldVisible = true
|
previewFieldVisible = true
|
||||||
|
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
content {
|
content {
|
||||||
// This card is temporary and is therefore not using a string resource
|
// This card is temporary and is therefore not using a string resource
|
||||||
FlorisErrorCard(
|
FlorisErrorCard(
|
||||||
@ -159,12 +164,20 @@ fun TypingScreen() = FlorisScreen {
|
|||||||
)
|
)
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
prefs.spelling.useUdmEntries,
|
prefs.spelling.useUdmEntries,
|
||||||
icon = Icons.Default.LibraryBooks,
|
icon = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||||
title = stringRes(R.string.pref__spelling__use_udm_entries__label),
|
title = stringRes(R.string.pref__spelling__use_udm_entries__label),
|
||||||
summary = stringRes(R.string.pref__spelling__use_udm_entries__summary),
|
summary = stringRes(R.string.pref__spelling__use_udm_entries__summary),
|
||||||
enabledIf = { florisSpellCheckerEnabled.value },
|
enabledIf = { florisSpellCheckerEnabled.value },
|
||||||
visibleIf = { false }, // For now
|
visibleIf = { false }, // For now
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PreferenceGroup(title = stringRes(R.string.settings__dictionary__title)) {
|
||||||
|
Preference(
|
||||||
|
icon = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||||
|
title = stringRes(R.string.settings__dictionary__title),
|
||||||
|
onClick = { navController.navigate(Routes.Settings.Dictionary) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,12 +42,10 @@ import androidx.compose.material.icons.filled.SelectAll
|
|||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
|
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Smartphone
|
|
||||||
import androidx.compose.material.icons.filled.SpaceBar
|
import androidx.compose.material.icons.filled.SpaceBar
|
||||||
import androidx.compose.material.icons.filled.Undo
|
import androidx.compose.material.icons.filled.Undo
|
||||||
import androidx.compose.material.icons.outlined.Assignment
|
import androidx.compose.material.icons.outlined.Assignment
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||||
@ -57,6 +55,7 @@ import dev.patrickgold.florisboard.ime.input.InputShiftState
|
|||||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||||
|
import dev.patrickgold.florisboard.lib.android.AndroidInternalR
|
||||||
import dev.patrickgold.jetpref.datastore.ui.vectorResource
|
import dev.patrickgold.jetpref.datastore.ui.vectorResource
|
||||||
|
|
||||||
interface ComputingEvaluator {
|
interface ComputingEvaluator {
|
||||||
@ -209,8 +208,7 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
|
|||||||
}
|
}
|
||||||
KeyCode.COMPACT_LAYOUT_TO_LEFT,
|
KeyCode.COMPACT_LAYOUT_TO_LEFT,
|
||||||
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> {
|
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> {
|
||||||
// TODO: find a better icon for compact mode
|
context()?.vectorResource(id = AndroidInternalR.drawable.ic_qs_one_handed_mode)
|
||||||
Icons.Default.Smartphone
|
|
||||||
}
|
}
|
||||||
KeyCode.VOICE_INPUT -> {
|
KeyCode.VOICE_INPUT -> {
|
||||||
Icons.Default.KeyboardVoice
|
Icons.Default.KeyboardVoice
|
||||||
@ -276,9 +274,9 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
|
|||||||
}
|
}
|
||||||
KeyCode.TOGGLE_INCOGNITO_MODE -> {
|
KeyCode.TOGGLE_INCOGNITO_MODE -> {
|
||||||
if (evaluator.state.isIncognitoMode) {
|
if (evaluator.state.isIncognitoMode) {
|
||||||
ImageVector.vectorResource(theme = null, resId = R.drawable.ic_incognito, res = this.context()?.resources!!)
|
this.context()?.vectorResource(id = R.drawable.ic_incognito)
|
||||||
} else {
|
} else {
|
||||||
ImageVector.vectorResource(theme = null, resId = R.drawable.ic_incognito_off, res = this.context()?.resources!!)
|
this.context()?.vectorResource(id = R.drawable.ic_incognito_off)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode.TOGGLE_AUTOCORRECT -> {
|
KeyCode.TOGGLE_AUTOCORRECT -> {
|
||||||
|
@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.keyboard
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||||
import dev.patrickgold.florisboard.appContext
|
import dev.patrickgold.florisboard.appContext
|
||||||
import dev.patrickgold.florisboard.assetManager
|
|
||||||
import dev.patrickgold.florisboard.extensionManager
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||||
import dev.patrickgold.florisboard.ime.popup.PopupMapping
|
import dev.patrickgold.florisboard.ime.popup.PopupMapping
|
||||||
@ -34,6 +33,7 @@ import dev.patrickgold.florisboard.lib.devtools.flogDebug
|
|||||||
import dev.patrickgold.florisboard.lib.devtools.flogWarning
|
import dev.patrickgold.florisboard.lib.devtools.flogWarning
|
||||||
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
import dev.patrickgold.florisboard.lib.io.ZipUtils
|
import dev.patrickgold.florisboard.lib.io.ZipUtils
|
||||||
|
import dev.patrickgold.florisboard.lib.io.loadJsonAsset
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -69,7 +69,6 @@ private data class CachedPopupMapping(
|
|||||||
class LayoutManager(context: Context) {
|
class LayoutManager(context: Context) {
|
||||||
private val prefs by florisPreferenceModel()
|
private val prefs by florisPreferenceModel()
|
||||||
private val appContext by context.appContext()
|
private val appContext by context.appContext()
|
||||||
private val assetManager by context.assetManager()
|
|
||||||
private val extensionManager by context.extensionManager()
|
private val extensionManager by context.extensionManager()
|
||||||
private val keyboardManager by context.keyboardManager()
|
private val keyboardManager by context.keyboardManager()
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ class LayoutManager(context: Context) {
|
|||||||
val layout = async {
|
val layout = async {
|
||||||
runCatching {
|
runCatching {
|
||||||
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
|
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
|
||||||
val arrangement = assetManager.loadJsonAsset<LayoutArrangement>(jsonStr).getOrThrow()
|
val arrangement = loadJsonAsset<LayoutArrangement>(jsonStr).getOrThrow()
|
||||||
CachedLayout(ltn.type, ltn.name, meta, arrangement)
|
CachedLayout(ltn.type, ltn.name, meta, arrangement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,7 +127,7 @@ class LayoutManager(context: Context) {
|
|||||||
val popupMapping = async {
|
val popupMapping = async {
|
||||||
runCatching {
|
runCatching {
|
||||||
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
|
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
|
||||||
val mapping = assetManager.loadJsonAsset<PopupMapping>(jsonStr).getOrThrow()
|
val mapping = loadJsonAsset<PopupMapping>(jsonStr).getOrThrow()
|
||||||
CachedPopupMapping(name, meta, mapping)
|
CachedPopupMapping(name, meta, mapping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,4 +51,11 @@ object AndroidInternalR {
|
|||||||
Resources.getSystem().getIdentifier("ime_action_default", "string", "android")
|
Resources.getSystem().getIdentifier("ime_action_default", "string", "android")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@SuppressLint("DiscouragedApi")
|
||||||
|
@Suppress("ClassName")
|
||||||
|
object drawable {
|
||||||
|
val ic_qs_one_handed_mode by lazy {
|
||||||
|
Resources.getSystem().getIdentifier("ic_qs_one_handed_mode", "drawable", "android")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
package dev.patrickgold.florisboard.lib.ext
|
package dev.patrickgold.florisboard.lib.ext
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import dev.patrickgold.florisboard.BuildConfig
|
||||||
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
||||||
import dev.patrickgold.florisboard.lib.io.FsDir
|
import dev.patrickgold.florisboard.lib.io.FsDir
|
||||||
import dev.patrickgold.florisboard.lib.io.FsFile
|
import dev.patrickgold.florisboard.lib.io.FsFile
|
||||||
@ -113,6 +115,37 @@ abstract class Extension {
|
|||||||
abstract fun edit(): ExtensionEditor
|
abstract fun edit(): ExtensionEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an update url for [Extension] lists.
|
||||||
|
*
|
||||||
|
* @param version the version of the api path
|
||||||
|
* @param host the host for the addons store
|
||||||
|
* @return the Url
|
||||||
|
*/
|
||||||
|
internal fun List<Extension>.generateUpdateUrl(
|
||||||
|
version: String = BuildConfig.FLADDONS_API_VERSION,
|
||||||
|
host: String = BuildConfig.FLADDONS_STORE_URL,
|
||||||
|
): String {
|
||||||
|
return Uri.Builder().run {
|
||||||
|
scheme("https")
|
||||||
|
authority(host)
|
||||||
|
appendPath("updates")
|
||||||
|
appendPath(version)
|
||||||
|
encodedFragment(
|
||||||
|
buildString {
|
||||||
|
append("data={")
|
||||||
|
for (extension in this@generateUpdateUrl) {
|
||||||
|
append(extension.meta.getUpdateJsonPair())
|
||||||
|
if (extension != this@generateUpdateUrl.last()) {
|
||||||
|
append(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append("}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.build().toString()
|
||||||
|
}
|
||||||
|
|
||||||
interface ExtensionEditor {
|
interface ExtensionEditor {
|
||||||
var meta: ExtensionMeta
|
var meta: ExtensionMeta
|
||||||
val dependencies: MutableList<String>
|
val dependencies: MutableList<String>
|
||||||
|
@ -19,9 +19,9 @@ package dev.patrickgold.florisboard.lib.ext
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.FileObserver
|
import android.os.FileObserver
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import dev.patrickgold.florisboard.appContext
|
import dev.patrickgold.florisboard.appContext
|
||||||
import dev.patrickgold.florisboard.assetManager
|
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||||
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
import dev.patrickgold.florisboard.ime.text.composing.Appender
|
import dev.patrickgold.florisboard.ime.text.composing.Appender
|
||||||
@ -37,7 +37,12 @@ import dev.patrickgold.florisboard.lib.devtools.flogError
|
|||||||
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
||||||
import dev.patrickgold.florisboard.lib.io.FsFile
|
import dev.patrickgold.florisboard.lib.io.FsFile
|
||||||
import dev.patrickgold.florisboard.lib.io.ZipUtils
|
import dev.patrickgold.florisboard.lib.io.ZipUtils
|
||||||
|
import dev.patrickgold.florisboard.lib.io.delete
|
||||||
|
import dev.patrickgold.florisboard.lib.io.listDirs
|
||||||
|
import dev.patrickgold.florisboard.lib.io.listFiles
|
||||||
|
import dev.patrickgold.florisboard.lib.io.loadJsonAsset
|
||||||
import dev.patrickgold.florisboard.lib.io.writeJson
|
import dev.patrickgold.florisboard.lib.io.writeJson
|
||||||
|
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -86,13 +91,17 @@ class ExtensionManager(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val appContext by context.appContext()
|
private val appContext by context.appContext()
|
||||||
private val assetManager by context.assetManager()
|
|
||||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH)
|
val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH)
|
||||||
val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_PATH)
|
val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_PATH)
|
||||||
val languagePacks = ExtensionIndex(LanguagePackExtension.serializer(), IME_LANGUAGEPACK_PATH)
|
val languagePacks = ExtensionIndex(LanguagePackExtension.serializer(), IME_LANGUAGEPACK_PATH)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun combinedExtensionList() = listOf(keyboardExtensions.observeAsNonNullState(), themes.observeAsNonNullState(), languagePacks.observeAsNonNullState()).map {
|
||||||
|
it.value
|
||||||
|
}.flatten()
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
keyboardExtensions.init()
|
keyboardExtensions.init()
|
||||||
themes.init()
|
themes.init()
|
||||||
@ -142,7 +151,7 @@ class ExtensionManager(context: Context) {
|
|||||||
fun delete(ext: Extension) {
|
fun delete(ext: Extension) {
|
||||||
check(canDelete(ext)) { "Cannot delete extension!" }
|
check(canDelete(ext)) { "Cannot delete extension!" }
|
||||||
ext.unload(appContext)
|
ext.unload(appContext)
|
||||||
assetManager.delete(ext.sourceRef!!)
|
ext.sourceRef!!.delete(appContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ExtensionIndex<T : Extension>(
|
inner class ExtensionIndex<T : Extension>(
|
||||||
@ -200,11 +209,11 @@ class ExtensionManager(context: Context) {
|
|||||||
|
|
||||||
private fun indexAssetsModule(): List<T> {
|
private fun indexAssetsModule(): List<T> {
|
||||||
val list = mutableListOf<T>()
|
val list = mutableListOf<T>()
|
||||||
assetManager.listDirs(assetsModuleRef).fold(
|
assetsModuleRef.listDirs(appContext).fold(
|
||||||
onSuccess = { extRefs ->
|
onSuccess = { extRefs ->
|
||||||
for (extRef in extRefs) {
|
for (extRef in extRefs) {
|
||||||
val fileRef = extRef.subRef(ExtensionDefaults.MANIFEST_FILE_NAME)
|
val fileRef = extRef.subRef(ExtensionDefaults.MANIFEST_FILE_NAME)
|
||||||
assetManager.loadJsonAsset(fileRef, serializer, ExtensionJsonConfig).fold(
|
fileRef.loadJsonAsset(appContext, serializer, ExtensionJsonConfig).fold(
|
||||||
onSuccess = { ext ->
|
onSuccess = { ext ->
|
||||||
ext.sourceRef = extRef
|
ext.sourceRef = extRef
|
||||||
list.add(ext)
|
list.add(ext)
|
||||||
@ -224,7 +233,7 @@ class ExtensionManager(context: Context) {
|
|||||||
|
|
||||||
private fun indexInternalModule(): List<T> {
|
private fun indexInternalModule(): List<T> {
|
||||||
val list = mutableListOf<T>()
|
val list = mutableListOf<T>()
|
||||||
assetManager.listFiles(internalModuleRef).fold(
|
internalModuleRef.listFiles(appContext).fold(
|
||||||
onSuccess = { extRefs ->
|
onSuccess = { extRefs ->
|
||||||
for (extRef in extRefs) {
|
for (extRef in extRefs) {
|
||||||
val fileRef = extRef.absoluteFile(appContext)
|
val fileRef = extRef.absoluteFile(appContext)
|
||||||
@ -233,7 +242,7 @@ class ExtensionManager(context: Context) {
|
|||||||
}
|
}
|
||||||
ZipUtils.readFileFromArchive(appContext, extRef, ExtensionDefaults.MANIFEST_FILE_NAME).fold(
|
ZipUtils.readFileFromArchive(appContext, extRef, ExtensionDefaults.MANIFEST_FILE_NAME).fold(
|
||||||
onSuccess = { metaStr ->
|
onSuccess = { metaStr ->
|
||||||
assetManager.loadJsonAsset(metaStr, serializer, ExtensionJsonConfig).fold(
|
loadJsonAsset(metaStr, serializer, ExtensionJsonConfig).fold(
|
||||||
onSuccess = { ext ->
|
onSuccess = { ext ->
|
||||||
ext.sourceRef = extRef
|
ext.sourceRef = extRef
|
||||||
list.add(ext)
|
list.add(ext)
|
||||||
|
@ -101,4 +101,8 @@ data class ExtensionMeta(
|
|||||||
* Use an SPDX license expression if this extension has multiple licenses.
|
* Use an SPDX license expression if this extension has multiple licenses.
|
||||||
*/
|
*/
|
||||||
val license: String,
|
val license: String,
|
||||||
)
|
) {
|
||||||
|
fun getUpdateJsonPair(): String {
|
||||||
|
return "\"${id}\":\"${version}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package dev.patrickgold.florisboard.lib.ext
|
package dev.patrickgold.florisboard.lib.ext
|
||||||
|
|
||||||
import androidx.core.text.trimmedLength
|
import androidx.core.text.trimmedLength
|
||||||
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||||
import dev.patrickgold.florisboard.lib.ValidationRule
|
import dev.patrickgold.florisboard.lib.ValidationRule
|
||||||
import dev.patrickgold.florisboard.lib.snygg.SnyggStylesheet
|
import dev.patrickgold.florisboard.lib.snygg.SnyggStylesheet
|
||||||
@ -25,8 +26,6 @@ import dev.patrickgold.florisboard.lib.snygg.value.SnyggPercentShapeValue
|
|||||||
import dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue
|
import dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue
|
||||||
import dev.patrickgold.florisboard.lib.validate
|
import dev.patrickgold.florisboard.lib.validate
|
||||||
|
|
||||||
// TODO: (priority=medium)
|
|
||||||
// make all strings available for localize
|
|
||||||
object ExtensionValidation {
|
object ExtensionValidation {
|
||||||
private val MetaIdRegex = """^[a-z][a-z0-9_]*(\.[a-z0-9][a-z0-9_]*)*${'$'}""".toRegex()
|
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 ComponentIdRegex = """^[a-z][a-z0-9_]*${'$'}""".toRegex()
|
||||||
@ -38,9 +37,9 @@ object ExtensionValidation {
|
|||||||
forProperty = "id"
|
forProperty = "id"
|
||||||
validator { str ->
|
validator { str ->
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a package name")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_package_name)
|
||||||
MetaIdRegex.matches(str) -> resultValid()
|
MetaIdRegex.matches(str) -> resultValid()
|
||||||
else -> resultInvalid("Package name does not match regex $MetaIdRegex")
|
else -> resultInvalid(error = R.string.ext__validation__error_package_name, "id_regex" to MetaIdRegex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +49,7 @@ object ExtensionValidation {
|
|||||||
forProperty = "version"
|
forProperty = "version"
|
||||||
validator { str ->
|
validator { str ->
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a version")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_version)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,7 +60,7 @@ object ExtensionValidation {
|
|||||||
forProperty = "title"
|
forProperty = "title"
|
||||||
validator { str ->
|
validator { str ->
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a title")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_title)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +72,7 @@ object ExtensionValidation {
|
|||||||
validator { str ->
|
validator { str ->
|
||||||
val maintainers = str.lines().filter { it.isNotBlank() }
|
val maintainers = str.lines().filter { it.isNotBlank() }
|
||||||
when {
|
when {
|
||||||
maintainers.isEmpty() -> resultInvalid(error = "Please enter at least one valid maintainer")
|
maintainers.isEmpty() -> resultInvalid(error = R.string.ext__validation__enter_maintainer)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,7 +83,7 @@ object ExtensionValidation {
|
|||||||
forProperty = "license"
|
forProperty = "license"
|
||||||
validator { str ->
|
validator { str ->
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a license identifier")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_license)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,8 +94,8 @@ object ExtensionValidation {
|
|||||||
forProperty = "id"
|
forProperty = "id"
|
||||||
validator { str ->
|
validator { str ->
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a component ID")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_component_id)
|
||||||
!ComponentIdRegex.matches(str) -> resultInvalid(error = "Please enter a component ID matching $ComponentIdRegex")
|
!ComponentIdRegex.matches(str) -> resultInvalid(error = R.string.ext__validation__error_component_id, "component_id_regex" to ComponentIdRegex)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,8 +106,8 @@ object ExtensionValidation {
|
|||||||
forProperty = "label"
|
forProperty = "label"
|
||||||
validator { str ->
|
validator { str ->
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a component label")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_component_label)
|
||||||
str.trimmedLength() > 30 -> resultValid(hint = "Your component label is quite long, which may lead to clipping in the UI")
|
str.trimmedLength() > 30 -> resultValid(hint = R.string.ext__validation__hint_component_label_to_long)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +119,7 @@ object ExtensionValidation {
|
|||||||
validator { str ->
|
validator { str ->
|
||||||
val authors = str.lines().filter { it.isNotBlank() }
|
val authors = str.lines().filter { it.isNotBlank() }
|
||||||
when {
|
when {
|
||||||
authors.isEmpty() -> resultInvalid(error = "Please enter at least one valid author")
|
authors.isEmpty() -> resultInvalid(error = R.string.ext__validation__error_author)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,9 +131,9 @@ object ExtensionValidation {
|
|||||||
validator { str ->
|
validator { str ->
|
||||||
when {
|
when {
|
||||||
str.isEmpty() -> resultValid()
|
str.isEmpty() -> resultValid()
|
||||||
str.isBlank() -> resultInvalid(error = "The stylesheet path must not be blank")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__error_stylesheet_path_blank)
|
||||||
!ThemeComponentStylesheetPathRegex.matches(str) -> {
|
!ThemeComponentStylesheetPathRegex.matches(str) -> {
|
||||||
resultInvalid(error = "Please enter a valid stylesheet path matching $ThemeComponentStylesheetPathRegex")
|
resultInvalid(error = R.string.ext__validation__error_stylesheet_path, "stylesheet_path_regex" to ThemeComponentStylesheetPathRegex)
|
||||||
}
|
}
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
@ -147,12 +146,12 @@ object ExtensionValidation {
|
|||||||
validator { input ->
|
validator { input ->
|
||||||
val str = input.trim()
|
val str = input.trim()
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a variable name")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_property)
|
||||||
str == "-" || str.startsWith("--") -> resultValid()
|
str == "-" || str.startsWith("--") -> resultValid()
|
||||||
!ThemeComponentVariableNameRegex.matches(str) -> {
|
!ThemeComponentVariableNameRegex.matches(str) -> {
|
||||||
resultInvalid(error = "Please enter a valid variable name matching $ThemeComponentVariableNameRegex")
|
resultInvalid(error = R.string.ext__validation__error_property, "variable_name_regex" to ThemeComponentVariableNameRegex)
|
||||||
}
|
}
|
||||||
else -> resultValid(hint = "By convention a FlorisCSS variable name starts with two dashes (--)")
|
else -> resultValid(hint = R.string.ext__validation__hint_property)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,9 +162,9 @@ object ExtensionValidation {
|
|||||||
validator { input ->
|
validator { input ->
|
||||||
val str = input.trim()
|
val str = input.trim()
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a color string")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_color)
|
||||||
dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue.deserialize(str).isFailure -> {
|
dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue.deserialize(str).isFailure -> {
|
||||||
resultInvalid(error = "Please enter a valid color string")
|
resultInvalid(error = R.string.ext__validation__error_color)
|
||||||
}
|
}
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
@ -178,9 +177,9 @@ object ExtensionValidation {
|
|||||||
validator { str ->
|
validator { str ->
|
||||||
val floatValue = str.toFloatOrNull()
|
val floatValue = str.toFloatOrNull()
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a dp size")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_dp_size)
|
||||||
floatValue == null -> resultInvalid(error = "Please enter a valid number")
|
floatValue == null -> resultInvalid(error = R.string.ext__validation__enter_valid_number)
|
||||||
floatValue < 0f -> resultInvalid(error = "Please enter a positive number (>=0)")
|
floatValue < 0f -> resultInvalid(error = R.string.ext__validation__enter_positive_number)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,10 +191,10 @@ object ExtensionValidation {
|
|||||||
validator { str ->
|
validator { str ->
|
||||||
val intValue = str.toIntOrNull()
|
val intValue = str.toIntOrNull()
|
||||||
when {
|
when {
|
||||||
str.isBlank() -> resultInvalid(error = "Please enter a percent size")
|
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_percent_size)
|
||||||
intValue == null -> resultInvalid(error = "Please enter a valid number")
|
intValue == null -> resultInvalid(error = R.string.ext__validation__enter_valid_number)
|
||||||
intValue < 0 || intValue > 100 -> resultInvalid(error = "Please enter a positive number between 0 and 100")
|
intValue < 0 || intValue > 100 -> resultInvalid(error = R.string.ext__validation__enter_number_between_0_100)
|
||||||
intValue > 50 -> resultValid(hint = "Any value above 50% will behave as if you set 50%, consider lowering your percent size")
|
intValue > 50 -> resultValid(hint = R.string.ext__validation__hint_value_above_50_percent)
|
||||||
else -> resultValid()
|
else -> resultValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package dev.patrickgold.florisboard.lib.io
|
package dev.patrickgold.florisboard.lib.io
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dev.patrickgold.florisboard.appContext
|
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.AbstractKeyData
|
import dev.patrickgold.florisboard.ime.keyboard.AbstractKeyData
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.CaseSelector
|
import dev.patrickgold.florisboard.ime.keyboard.CaseSelector
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.CharWidthSelector
|
import dev.patrickgold.florisboard.ime.keyboard.CharWidthSelector
|
||||||
@ -66,122 +65,118 @@ val DefaultJsonConfig = Json {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fully deprecate and remove this class, should be substituted by extension funs on the actual file and stream
|
fun FlorisRef.delete(context: Context) {
|
||||||
// instances
|
when {
|
||||||
class AssetManager(context: Context) {
|
isCache || isInternal -> {
|
||||||
val appContext by context.appContext()
|
absoluteFile(context).delete()
|
||||||
|
|
||||||
fun delete(ref: FlorisRef) {
|
|
||||||
when {
|
|
||||||
ref.isCache || ref.isInternal -> {
|
|
||||||
ref.absoluteFile(appContext).delete()
|
|
||||||
}
|
|
||||||
else -> error("Can not delete directory/file in location '${ref.scheme}'.")
|
|
||||||
}
|
}
|
||||||
}
|
else -> error("Can not delete directory/file in location '${scheme}'.")
|
||||||
|
|
||||||
fun hasAsset(ref: FlorisRef): Boolean {
|
|
||||||
return when {
|
|
||||||
ref.isAssets -> {
|
|
||||||
try {
|
|
||||||
val file = File(ref.relativePath)
|
|
||||||
val list = appContext.assets.list(file.parent?.toString() ?: "")
|
|
||||||
list?.contains(file.name) == true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ref.isCache || ref.isInternal -> {
|
|
||||||
val file = File(ref.absolutePath(appContext))
|
|
||||||
file.exists() && file.isFile
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun list(ref: FlorisRef) = list(ref, files = true, dirs = true)
|
|
||||||
|
|
||||||
fun listFiles(ref: FlorisRef) = list(ref, files = true, dirs = false)
|
|
||||||
|
|
||||||
fun listDirs(ref: FlorisRef) = list(ref, files = false, dirs = true)
|
|
||||||
|
|
||||||
private fun list(ref: FlorisRef, files: Boolean, dirs: Boolean) = runCatching<List<FlorisRef>> {
|
|
||||||
when {
|
|
||||||
!files && !dirs -> listOf()
|
|
||||||
ref.isAssets -> {
|
|
||||||
appContext.assets.list(ref.relativePath)?.mapNotNull { fileName ->
|
|
||||||
val subList = appContext.assets.list("${ref.relativePath}/$fileName") ?: return@mapNotNull null
|
|
||||||
when {
|
|
||||||
files && dirs || files && subList.isEmpty() || dirs && subList.isNotEmpty() -> {
|
|
||||||
ref.subRef(fileName)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
} ?: listOf()
|
|
||||||
}
|
|
||||||
ref.isCache || ref.isInternal -> {
|
|
||||||
val dir = ref.absoluteFile(appContext)
|
|
||||||
if (dir.isDirectory) {
|
|
||||||
when {
|
|
||||||
files && dirs -> dir.listFiles()?.toList()
|
|
||||||
files -> dir.listFiles()?.filter { it.isFile }
|
|
||||||
dirs -> dir.listFiles()?.filter { it.isDirectory }
|
|
||||||
else -> null
|
|
||||||
}!!.map { ref.subRef(it.name) }
|
|
||||||
} else {
|
|
||||||
listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> error("Unsupported FlorisRef source!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> loadJsonAsset(
|
|
||||||
ref: FlorisRef,
|
|
||||||
serializer: KSerializer<T>,
|
|
||||||
jsonConfig: Json = DefaultJsonConfig,
|
|
||||||
) = runCatching<T> {
|
|
||||||
val jsonStr = loadTextAsset(ref).getOrThrow()
|
|
||||||
jsonConfig.decodeFromString(serializer, jsonStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> loadJsonAsset(jsonStr: String, jsonConfig: Json = DefaultJsonConfig): Result<T> {
|
|
||||||
return runCatching { jsonConfig.decodeFromString(jsonStr) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> loadJsonAsset(
|
|
||||||
jsonStr: String,
|
|
||||||
serializer: KSerializer<T>,
|
|
||||||
jsonConfig: Json = DefaultJsonConfig,
|
|
||||||
) = runCatching<T> {
|
|
||||||
jsonConfig.decodeFromString(serializer, jsonStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadTextAsset(ref: FlorisRef): Result<String> {
|
|
||||||
return when {
|
|
||||||
ref.isAssets -> runCatching {
|
|
||||||
appContext.assets.reader(ref.relativePath).use { it.readText() }
|
|
||||||
}
|
|
||||||
ref.isCache || ref.isInternal -> {
|
|
||||||
val file = File(ref.absolutePath(appContext))
|
|
||||||
val contents = readTextFile(file).getOrElse { return resultErr(it) }
|
|
||||||
if (contents.isBlank()) {
|
|
||||||
resultErrStr("File is blank!")
|
|
||||||
} else {
|
|
||||||
resultOk(contents)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> resultErrStr("Unsupported asset ref!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a given [file] and returns its content.
|
|
||||||
*
|
|
||||||
* @param file The file object.
|
|
||||||
* @return The contents of the file or an empty string, if the file does not exist.
|
|
||||||
*/
|
|
||||||
private fun readTextFile(file: File) = runCatching {
|
|
||||||
file.readText(Charsets.UTF_8)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun FlorisRef.hasAsset(context: Context): Boolean {
|
||||||
|
return when {
|
||||||
|
isAssets -> {
|
||||||
|
try {
|
||||||
|
val file = File(relativePath)
|
||||||
|
val list = context.assets.list(file.parent?.toString() ?: "")
|
||||||
|
list?.contains(file.name) == true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isCache || isInternal -> {
|
||||||
|
val file = File(absolutePath(context))
|
||||||
|
file.exists() && file.isFile
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun FlorisRef.list(context: Context) = list(context, files = true, dirs = true)
|
||||||
|
|
||||||
|
fun FlorisRef.listFiles(context: Context) = list(context, files = true, dirs = false)
|
||||||
|
|
||||||
|
fun FlorisRef.listDirs(context: Context) = list(context, files = false, dirs = true)
|
||||||
|
|
||||||
|
private fun FlorisRef.list(appContext: Context, files: Boolean, dirs: Boolean) = runCatching<List<FlorisRef>> {
|
||||||
|
when {
|
||||||
|
!files && !dirs -> listOf()
|
||||||
|
isAssets -> {
|
||||||
|
appContext.assets.list(relativePath)?.mapNotNull { fileName ->
|
||||||
|
val subList = appContext.assets.list("${relativePath}/$fileName") ?: return@mapNotNull null
|
||||||
|
when {
|
||||||
|
files && dirs || files && subList.isEmpty() || dirs && subList.isNotEmpty() -> {
|
||||||
|
subRef(fileName)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} ?: listOf()
|
||||||
|
}
|
||||||
|
isCache || isInternal -> {
|
||||||
|
val dir = absoluteFile(appContext)
|
||||||
|
if (dir.isDirectory) {
|
||||||
|
when {
|
||||||
|
files && dirs -> dir.listFiles()?.toList()
|
||||||
|
files -> dir.listFiles()?.filter { it.isFile }
|
||||||
|
dirs -> dir.listFiles()?.filter { it.isDirectory }
|
||||||
|
else -> null
|
||||||
|
}!!.map { subRef(it.name) }
|
||||||
|
} else {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> error("Unsupported FlorisRef source!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> FlorisRef.loadJsonAsset(
|
||||||
|
context: Context,
|
||||||
|
serializer: KSerializer<T>,
|
||||||
|
jsonConfig: Json = DefaultJsonConfig,
|
||||||
|
) = runCatching<T> {
|
||||||
|
val jsonStr = loadTextAsset(context).getOrThrow()
|
||||||
|
jsonConfig.decodeFromString(serializer, jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> loadJsonAsset(jsonStr: String, jsonConfig: Json = DefaultJsonConfig): Result<T> {
|
||||||
|
return runCatching { jsonConfig.decodeFromString(jsonStr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> loadJsonAsset(
|
||||||
|
jsonStr: String,
|
||||||
|
serializer: KSerializer<T>,
|
||||||
|
jsonConfig: Json = DefaultJsonConfig,
|
||||||
|
) = runCatching<T> {
|
||||||
|
jsonConfig.decodeFromString(serializer, jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun FlorisRef.loadTextAsset(context: Context): Result<String> {
|
||||||
|
return when {
|
||||||
|
isAssets -> runCatching {
|
||||||
|
context.assets.reader(relativePath).use { it.readText() }
|
||||||
|
}
|
||||||
|
isCache || isInternal -> {
|
||||||
|
val file = File(absolutePath(context))
|
||||||
|
val contents = readTextFile(file).getOrElse { return resultErr(it) }
|
||||||
|
if (contents.isBlank()) {
|
||||||
|
resultErrStr("File is blank!")
|
||||||
|
} else {
|
||||||
|
resultOk(contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> resultErrStr("Unsupported asset ref!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a given [file] and returns its content.
|
||||||
|
*
|
||||||
|
* @param file The file object.
|
||||||
|
* @return The contents of the file or an empty string, if the file does not exist.
|
||||||
|
*/
|
||||||
|
private fun readTextFile(file: File) = runCatching {
|
||||||
|
file.readText(Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ package dev.patrickgold.florisboard.lib.io
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import dev.patrickgold.florisboard.assetManager
|
|
||||||
import dev.patrickgold.florisboard.lib.android.copyRecursively
|
import dev.patrickgold.florisboard.lib.android.copyRecursively
|
||||||
import dev.patrickgold.florisboard.lib.android.write
|
import dev.patrickgold.florisboard.lib.android.write
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@ -28,10 +27,9 @@ import java.util.zip.ZipOutputStream
|
|||||||
|
|
||||||
object ZipUtils {
|
object ZipUtils {
|
||||||
fun readFileFromArchive(context: Context, zipRef: FlorisRef, relPath: String) = runCatching<String> {
|
fun readFileFromArchive(context: Context, zipRef: FlorisRef, relPath: String) = runCatching<String> {
|
||||||
val assetManager by context.assetManager()
|
|
||||||
when {
|
when {
|
||||||
zipRef.isAssets -> {
|
zipRef.isAssets -> {
|
||||||
assetManager.loadTextAsset(zipRef.subRef(relPath)).getOrThrow()
|
zipRef.subRef(relPath).loadTextAsset(context).getOrThrow()
|
||||||
}
|
}
|
||||||
zipRef.isCache || zipRef.isInternal -> {
|
zipRef.isCache || zipRef.isInternal -> {
|
||||||
val flexHandle = FsFile(zipRef.absolutePath(context))
|
val flexHandle = FsFile(zipRef.absolutePath(context))
|
||||||
@ -141,7 +139,18 @@ object ZipUtils {
|
|||||||
val flexEntries = flexFile.entries()
|
val flexEntries = flexFile.entries()
|
||||||
while (flexEntries.hasMoreElements()) {
|
while (flexEntries.hasMoreElements()) {
|
||||||
val flexEntry = flexEntries.nextElement()
|
val flexEntry = flexEntries.nextElement()
|
||||||
|
if (flexEntry.name.length > 255) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
val flexEntryFile = FsFile(dstDir, flexEntry.name)
|
val flexEntryFile = FsFile(dstDir, flexEntry.name)
|
||||||
|
val canonicalDestinationDirPath = dstDir.canonicalPath
|
||||||
|
val canonicalDestinationFilePath = flexEntryFile.canonicalPath
|
||||||
|
if (canonicalDestinationFilePath.length > 1023) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!canonicalDestinationFilePath.startsWith(canonicalDestinationDirPath + FsFile.separator)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (flexEntry.isDirectory) {
|
if (flexEntry.isDirectory) {
|
||||||
flexEntryFile.mkdir()
|
flexEntryFile.mkdir()
|
||||||
} else {
|
} else {
|
||||||
@ -153,6 +162,9 @@ object ZipUtils {
|
|||||||
|
|
||||||
private fun ZipFile.copy(srcEntry: ZipEntry, dstFile: FsFile) {
|
private fun ZipFile.copy(srcEntry: ZipEntry, dstFile: FsFile) {
|
||||||
dstFile.outputStream().use { outStream ->
|
dstFile.outputStream().use { outStream ->
|
||||||
|
if (srcEntry.size > 100000000) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.getInputStream(srcEntry).use { inStream ->
|
this.getInputStream(srcEntry).use { inStream ->
|
||||||
inStream.copyTo(outStream)
|
inStream.copyTo(outStream)
|
||||||
}
|
}
|
||||||
|
@ -139,16 +139,13 @@
|
|||||||
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Sunset time</string>
|
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Sunset time</string>
|
||||||
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Day theme</string>
|
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Day theme</string>
|
||||||
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Night theme</string>
|
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Night theme</string>
|
||||||
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Selected theme</string>
|
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Manage installed themes</string>
|
||||||
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Adapt colors to app</string>
|
|
||||||
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Theme colors adapt to those in the current app, if the target app supports this.</string>
|
|
||||||
<string name="pref__theme__source_assets" comment="Label for the theme source field">FlorisBoard App Assets</string>
|
<string name="pref__theme__source_assets" comment="Label for the theme source field">FlorisBoard App Assets</string>
|
||||||
<string name="pref__theme__source_internal" comment="Label for the theme source field">Internal Storage</string>
|
<string name="pref__theme__source_internal" comment="Label for the theme source field">Internal Storage</string>
|
||||||
<string name="pref__theme__source_external" comment="Label for the theme source field">External Provider</string>
|
<string name="pref__theme__source_external" comment="Label for the theme source field">External Provider</string>
|
||||||
|
|
||||||
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Select day theme</string>
|
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Select day theme</string>
|
||||||
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Select night theme</string>
|
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Select night theme</string>
|
||||||
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Manage installed themes</string>
|
|
||||||
|
|
||||||
<string name="settings__theme_editor__fine_tune__title">Fine tune editor</string>
|
<string name="settings__theme_editor__fine_tune__title">Fine tune editor</string>
|
||||||
<string name="settings__theme_editor__fine_tune__level">Editing level</string>
|
<string name="settings__theme_editor__fine_tune__level">Editing level</string>
|
||||||
@ -620,6 +617,10 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Extension strings -->
|
<!-- Extension strings -->
|
||||||
|
<string name="ext__home__title">Addons & Extensions</string>
|
||||||
|
<string name="ext__list__ext_theme">Theme extensions</string>
|
||||||
|
<string name="ext__list__ext_keyboard">Keyboard extensions</string>
|
||||||
|
<string name="ext__list__ext_languagepack">Language pack extensions</string>
|
||||||
<string name="ext__meta__authors">Authors</string>
|
<string name="ext__meta__authors">Authors</string>
|
||||||
<string name="ext__meta__components">Bundled components</string>
|
<string name="ext__meta__components">Bundled components</string>
|
||||||
<string name="ext__meta__components_theme">Bundled themes</string>
|
<string name="ext__meta__components_theme">Bundled themes</string>
|
||||||
@ -672,7 +673,39 @@
|
|||||||
<string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">Expected a media file (image, audio, font, etc.) but found an extension archive.</string>
|
<string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">Expected a media file (image, audio, font, etc.) but found an extension archive.</string>
|
||||||
<string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">Expected an extension archive but found a media file (image, audio, font, etc.).</string>
|
<string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">Expected an extension archive but found a media file (image, audio, font, etc.).</string>
|
||||||
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">An unexpected error occurred during import. The following details were provided:</string>
|
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">An unexpected error occurred during import. The following details were provided:</string>
|
||||||
|
<string name="ext__validation__enter_package_name">Please enter a package name</string>
|
||||||
|
<string name="ext__validation__error_package_name">Package name does not match regex {id_regex}</string>
|
||||||
|
<string name="ext__validation__enter_version">Please enter a version</string>
|
||||||
|
<string name="ext__validation__enter_title">Please enter a title</string>
|
||||||
|
<string name="ext__validation__enter_maintainer">Please enter at least one valid maintainer</string>
|
||||||
|
<string name="ext__validation__enter_license">Please enter a license identifier</string>
|
||||||
|
<string name="ext__validation__enter_component_id">Please enter a component ID</string>
|
||||||
|
<string name="ext__validation__error_component_id">Please enter a component ID matching {component_id_regex}</string>
|
||||||
|
<string name="ext__validation__enter_component_label">Please enter a component label</string>
|
||||||
|
<string name="ext__validation__hint_component_label_to_long">Your component label is quite long, which may lead to clipping in the UI</string>
|
||||||
|
<string name="ext__validation__error_author">Please enter at least one valid author</string>
|
||||||
|
<string name="ext__validation__error_stylesheet_path_blank">The stylesheet path must not be blank</string>
|
||||||
|
<string name="ext__validation__error_stylesheet_path">Please enter a valid stylesheet path matching {stylesheet_path_regex}</string>
|
||||||
|
<string name="ext__validation__enter_property">Please enter a variable name</string>
|
||||||
|
<string name="ext__validation__error_property">Please enter a valid variable name matching {variable_name_regex}</string>
|
||||||
|
<string name="ext__validation__hint_property" tools:ignore="TypographyDashes">By convention a FlorisCSS variable name starts with two dashes (--)</string>
|
||||||
|
<string name="ext__validation__enter_color">Please enter a color string</string>
|
||||||
|
<string name="ext__validation__error_color">Please enter a valid color string</string>
|
||||||
|
<string name="ext__validation__enter_dp_size">Please enter a dp size</string>
|
||||||
|
<string name="ext__validation__enter_valid_number">Please enter a valid number</string>
|
||||||
|
<string name="ext__validation__enter_positive_number">Please enter a positive number (>=0)</string>
|
||||||
|
<string name="ext__validation__enter_percent_size">Please enter a percent size</string>
|
||||||
|
<string name="ext__validation__enter_number_between_0_100">Please enter a positive number between 0 and 100</string>
|
||||||
|
<string name="ext__validation__hint_value_above_50_percent">Any value above 50% will behave as if you set 50%, consider lowering your percent size</string>
|
||||||
|
<string name="ext__update_box__internet_permission_hint">Since this app does not have Internet permission, updates for installed extensions must be checked manually.</string>
|
||||||
|
<string name="ext__update_box__search_for_updates">Search for Updates</string>
|
||||||
|
<string name="ext__addon_management_box__managing_placeholder">Managing {extensions}</string>
|
||||||
|
<string name="ext__addon_management_box__addon_manager_info">All tasks related to importing, exporting, creating, customizing, and removing extensions can be handled through the centralized addon manager.</string>
|
||||||
|
<string name="ext__addon_management_box__go_to_page">Go to {ext_home_title}</string>
|
||||||
|
<string name="ext__home__info">You can download and install extensions from the FlorisBoard Addons Store or import any extension file you have downloaded from the internet.</string>
|
||||||
|
<string name="ext__home__visit_store">Visit Addons Store</string>
|
||||||
|
<string name="ext__home__manage_extensions">Manage installed extensions</string>
|
||||||
|
<string name="ext__list__view_details">View details</string>
|
||||||
|
|
||||||
<!-- Action strings -->
|
<!-- Action strings -->
|
||||||
<string name="action__add">Add</string>
|
<string name="action__add">Add</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user