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)
|
||||
- Improvements in clipboard / emoji functionality (v0.4.0-beta01/beta02)
|
||||
- Prepare project to have native code implemented in [Rust](https://www.rust-lang.org/) (v0.4.0-beta02)
|
||||
- - Upgrade Settings UI to Material 3 (v0.4.0-beta03)
|
||||
- Add support for importing extensions via system file handler APIs (relevant for Addons store) (v0.4.0-beta03)
|
||||
|
||||
Note that the previous versioning scheme has been dropped in favor of using a major.minor.patch versioning scheme, so versions like `0.3.16` are a thing of the past :)
|
||||
|
||||
@ -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
|
||||
- Add Tablet mode / Optimizations for landscape input based on new keyboard layout engine
|
||||
- Reimplementation of glide typing with the new layout engine and predictive text core
|
||||
- Add support for importing extensions via system file handler APIs (relevant for Addons store)
|
||||
- Add support for any remaining new features introduced with Android 13
|
||||
|
||||
## 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**
|
||||
|
||||
- Upgrade Settings UI to Material 3
|
||||
- Full on-board layout editor which allows users to create their own layouts without writing a JSON file
|
||||
- Theme rework part II
|
||||
- Adaptive themes v2
|
||||
|
@ -64,6 +64,8 @@ android {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField("String", "BUILD_COMMIT_HASH", "\"${getGitCommitHash()}\"")
|
||||
buildConfigField("String", "FLADDONS_API_VERSION", "\"v~draft2\"")
|
||||
buildConfigField("String", "FLADDONS_STORE_URL", "\"fladdonstest.patrickgold.dev\"")
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
|
@ -84,10 +84,23 @@
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/FlorisAppTheme.Splash"
|
||||
android:exported="false">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<data android:scheme="florisboard" android:host="app-ui"/>
|
||||
</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>
|
||||
|
||||
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
|
||||
@ -106,24 +119,6 @@
|
||||
</intent-filter>
|
||||
</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 -->
|
||||
<activity
|
||||
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.flogError
|
||||
import dev.patrickgold.florisboard.lib.ext.ExtensionManager
|
||||
import dev.patrickgold.florisboard.lib.io.AssetManager
|
||||
import dev.patrickgold.florisboard.lib.io.deleteContentsRecursively
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import org.florisboard.lib.kotlin.tryOrNull
|
||||
@ -67,7 +66,6 @@ class FlorisApplication : Application() {
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val mainHandler by lazy { Handler(mainLooper) }
|
||||
|
||||
val assetManager = lazy { AssetManager(this) }
|
||||
val cacheManager = lazy { CacheManager(this) }
|
||||
val clipboardManager = lazy { ClipboardManager(this) }
|
||||
val editorInstance = lazy { EditorInstance(this) }
|
||||
@ -144,8 +142,6 @@ private tailrec fun Context.florisApplication(): FlorisApplication {
|
||||
|
||||
fun Context.appContext() = lazyOf(this.florisApplication())
|
||||
|
||||
fun Context.assetManager() = this.florisApplication().assetManager
|
||||
|
||||
fun Context.cacheManager() = this.florisApplication().cacheManager
|
||||
|
||||
fun Context.clipboardManager() = this.florisApplication().clipboardManager
|
||||
|
@ -642,19 +642,11 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "theme__mode",
|
||||
default = ThemeMode.FOLLOW_SYSTEM,
|
||||
)
|
||||
val dayThemeAdaptToApp = boolean(
|
||||
key = "theme__day_theme_adapt_to_app",
|
||||
default = false,
|
||||
)
|
||||
val dayThemeId = custom(
|
||||
key = "theme__day_theme_id",
|
||||
default = extCoreTheme("floris_day"),
|
||||
serializer = ExtensionComponentName.Serializer,
|
||||
)
|
||||
val nightThemeAdaptToApp = boolean(
|
||||
key = "theme__night_theme_adapt_to_app",
|
||||
default = false,
|
||||
)
|
||||
val nightThemeId = custom(
|
||||
key = "theme__night_theme_id",
|
||||
default = extCoreTheme("floris_night"),
|
||||
|
@ -17,6 +17,7 @@
|
||||
package dev.patrickgold.florisboard.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
@ -24,11 +25,11 @@ import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -41,7 +42,9 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.patrickgold.florisboard.R
|
||||
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.cacheManager
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.lib.android.hideAppIcon
|
||||
@ -70,9 +73,11 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
|
||||
|
||||
class FlorisAppActivity : ComponentActivity() {
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val cacheManager by cacheManager()
|
||||
private var appTheme by mutableStateOf(AppTheme.AUTO)
|
||||
private var showAppIcon = true
|
||||
private var resourcesContext by mutableStateOf(this as Context)
|
||||
private var fileImportIntent by mutableStateOf<Intent?>(null)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Splash screen should be installed before calling super.onCreate()
|
||||
@ -110,14 +115,15 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
setContent {
|
||||
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) {
|
||||
//SystemUiApp()
|
||||
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
|
||||
private fun AppContent() {
|
||||
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 {
|
||||
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.ext.ExtensionEditScreen
|
||||
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.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.settings.HomeScreen
|
||||
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 org.florisboard.lib.kotlin.curlyFormat
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@Suppress("FunctionName", "ConstPropertyName")
|
||||
object Routes {
|
||||
object Setup {
|
||||
const val Screen = "setup"
|
||||
@ -117,6 +120,14 @@ object Routes {
|
||||
}
|
||||
|
||||
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}"
|
||||
fun Edit(id: String, serialType: String? = null): String {
|
||||
return Edit.curlyFormat("id" to id, "serial_type" to (serialType ?: ""))
|
||||
@ -209,12 +220,20 @@ object Routes {
|
||||
}
|
||||
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 ->
|
||||
val extensionId = navBackStack.arguments?.getString("id")
|
||||
val serialType = navBackStack.arguments?.getString("serial_type")
|
||||
ExtensionEditScreen(
|
||||
id = extensionId.toString(),
|
||||
createSerialType = serialType.takeIf { it != null && it.isNotBlank() },
|
||||
createSerialType = serialType.takeIf { !it.isNullOrBlank() },
|
||||
)
|
||||
}
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -100,12 +99,6 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
val cacheManager by context.cacheManager()
|
||||
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 {
|
||||
return when {
|
||||
!FileRegistry.matchesFileFilter(fileInfo, type.supportedFiles) -> {
|
||||
@ -119,29 +112,37 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
NATIVE_NULLPTR.toInt()
|
||||
}
|
||||
}
|
||||
fileInfo.mediaType == FileRegistry.FlexExtension.mediaType -> {
|
||||
else -> { // ext == null
|
||||
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(
|
||||
contract = ActivityResultContracts.GetMultipleContents(),
|
||||
onResult = { uriList ->
|
||||
// If uri is null it indicates that the selection activity
|
||||
// was cancelled (mostly by pressing the back button), so
|
||||
// we don't display an error message here.
|
||||
if (uriList.isNullOrEmpty()) return@rememberLauncherForActivityResult
|
||||
if (uriList.isEmpty()) return@rememberLauncherForActivityResult
|
||||
importResult?.getOrNull()?.close()
|
||||
importResult = runCatching { cacheManager.readFromUriIntoCache(uriList) }.map { workspace ->
|
||||
workspace.inputFileInfos.forEach { fileInfo ->
|
||||
fileInfo.skipReason = getSkipReason(fileInfo)
|
||||
}
|
||||
workspace
|
||||
}
|
||||
importResult = runCatching { cacheManager.readFromUriIntoCache(uriList) }.mapSkipReasons()
|
||||
},
|
||||
)
|
||||
|
||||
@ -197,15 +198,17 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
}
|
||||
|
||||
content {
|
||||
FlorisOutlinedButton(
|
||||
onClick = {
|
||||
importLauncher.launch("*/*")
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = stringRes(R.string.action__select_files),
|
||||
)
|
||||
if (initUuid == null) {
|
||||
FlorisOutlinedButton(
|
||||
onClick = {
|
||||
importLauncher.launch("*/*")
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = stringRes(R.string.action__select_files),
|
||||
)
|
||||
}
|
||||
|
||||
val result = importResult
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,84 +16,135 @@
|
||||
|
||||
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.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.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.FlorisTextButton
|
||||
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||
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
|
||||
fun ExtensionListScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.about__title)
|
||||
fun ExtensionListScreen(type: ExtensionListScreenType, showUpdate: Boolean) = FlorisScreen {
|
||||
title = stringRes(type.titleResId)
|
||||
previewFieldVisible = false
|
||||
|
||||
/*val navController = LocalNavController.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(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp, bottom = 32.dp)
|
||||
) {
|
||||
FlorisAppIcon()
|
||||
Text(
|
||||
text = stringRes(R.string.floris_app_name),
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
)
|
||||
}
|
||||
Preference(
|
||||
icon = R.drawable.ic_info,
|
||||
title = stringRes(R.string.about__version__title),
|
||||
summary = appVersion,
|
||||
onClick = {
|
||||
try {
|
||||
val isImeSelected = InputMethodUtils.checkIsFlorisboardSelected(context)
|
||||
if (isImeSelected) {
|
||||
FlorisClipboardManager.getInstance().addNewPlaintext(appVersion)
|
||||
} else {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Florisboard version", appVersion)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
content {
|
||||
if (showUpdate) {
|
||||
UpdateBox(extensionIndex = extensionIndex)
|
||||
}
|
||||
for (ext in extensionIndex) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = ext.meta.title,
|
||||
subtitle = ext.meta.id,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
text = ext.meta.description ?: "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp),
|
||||
) {
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
navController.navigate(Routes.Ext.View(ext.meta.id))
|
||||
},
|
||||
icon = Icons.Outlined.Info,
|
||||
text = stringRes(id = R.string.ext__list__view_details),//stringRes(R.string.action__add),
|
||||
colors = ButtonDefaults.textButtonColors(),
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
FlorisTextButton(
|
||||
onClick = {
|
||||
navController.navigate(Routes.Ext.Edit(ext.meta.id))
|
||||
},
|
||||
icon = Icons.Default.Edit,
|
||||
text = stringRes(R.string.action__edit),
|
||||
enabled = extensionManager.canDelete(ext),
|
||||
)
|
||||
}
|
||||
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,
|
||||
title = stringRes(R.string.about__changelog__title),
|
||||
summary = stringRes(R.string.about__changelog__summary),
|
||||
onClick = { launchUrl(context, R.string.florisboard__changelog_url, arrayOf(BuildConfig.VERSION_NAME)) },
|
||||
)
|
||||
Preference(
|
||||
icon = R.drawable.ic_code,
|
||||
title = stringRes(R.string.about__repository__title),
|
||||
summary = stringRes(R.string.about__repository__summary),
|
||||
onClick = { launchUrl(context, R.string.florisboard__repo_url) },
|
||||
)
|
||||
Preference(
|
||||
icon = R.drawable.ic_policy,
|
||||
title = stringRes(R.string.about__privacy_policy__title),
|
||||
summary = stringRes(R.string.about__privacy_policy__summary),
|
||||
onClick = { launchUrl(context, R.string.florisboard__privacy_policy_url) },
|
||||
)
|
||||
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) },
|
||||
)*/
|
||||
}
|
||||
}
|
||||
|
||||
if (type.launchExtensionCreate != null) {
|
||||
floatingActionButton {
|
||||
ExtendedFloatingActionButton(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringRes(id = R.string.ext__editor__title_create_any),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringRes(id = R.string.ext__editor__title_create_any),
|
||||
)
|
||||
},
|
||||
shape = FloatingActionButtonDefaults.extendedFabShape,
|
||||
onClick = { type.launchExtensionCreate.invoke(navController) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,8 @@ package dev.patrickgold.florisboard.app.ext
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.Mail
|
||||
import androidx.compose.material.icons.outlined.Mail
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -32,13 +30,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.lib.android.launchUrl
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisChip
|
||||
import dev.patrickgold.florisboard.lib.ext.ExtensionMaintainer
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun ExtensionMaintainerChip(
|
||||
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.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.Language
|
||||
import androidx.compose.material.icons.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
|
||||
import androidx.compose.material.icons.filled.SmartButton
|
||||
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.Info
|
||||
import androidx.compose.material.icons.outlined.Keyboard
|
||||
@ -131,18 +130,13 @@ fun HomeScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__typing__title),
|
||||
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(
|
||||
icon = Icons.Default.Gesture,
|
||||
title = stringRes(R.string.settings__gestures__title),
|
||||
onClick = { navController.navigate(Routes.Settings.Gestures) },
|
||||
)
|
||||
Preference(
|
||||
icon = Icons.Outlined.Assignment,
|
||||
icon = Icons.AutoMirrored.Outlined.Assignment,
|
||||
title = stringRes(R.string.settings__clipboard__title),
|
||||
onClick = { navController.navigate(Routes.Settings.Clipboard) },
|
||||
)
|
||||
@ -152,9 +146,9 @@ fun HomeScreen() = FlorisScreen {
|
||||
onClick = { navController.navigate(Routes.Settings.Media) },
|
||||
)
|
||||
Preference(
|
||||
icon = Icons.Default.Adb,
|
||||
title = stringRes(R.string.devtools__title),
|
||||
onClick = { navController.navigate(Routes.Devtools.Home) },
|
||||
icon = Icons.Default.Extension,
|
||||
title = stringRes(R.string.ext__home__title),
|
||||
onClick = { navController.navigate(Routes.Ext.Home) },
|
||||
)
|
||||
Preference(
|
||||
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.sp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.assetManager
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
|
||||
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
||||
import dev.patrickgold.florisboard.lib.io.loadTextAsset
|
||||
|
||||
@Composable
|
||||
fun ProjectLicenseScreen() = FlorisScreen {
|
||||
@ -41,7 +41,6 @@ fun ProjectLicenseScreen() = FlorisScreen {
|
||||
scrollable = false
|
||||
|
||||
val context = LocalContext.current
|
||||
val assetManager by context.assetManager()
|
||||
|
||||
content {
|
||||
// Forcing LTR because the Apache 2.0 License shipped and displayed
|
||||
@ -54,8 +53,8 @@ fun ProjectLicenseScreen() = FlorisScreen {
|
||||
.florisVerticalScroll()
|
||||
.florisHorizontalScroll(),
|
||||
) {
|
||||
val licenseText = assetManager.loadTextAsset(
|
||||
FlorisRef.assets("license/project_license.txt")
|
||||
val licenseText = FlorisRef.assets("license/project_license.txt").loadTextAsset(
|
||||
context
|
||||
).getOrElse {
|
||||
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
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Adb
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.material.icons.filled.FormatPaint
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
@ -166,6 +167,11 @@ fun AdvancedScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.pref__advanced__incognito_mode__label),
|
||||
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)) {
|
||||
Preference(
|
||||
|
@ -16,83 +16,56 @@
|
||||
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
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.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.app.ext.ExtensionImportScreenType
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
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.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
|
||||
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.lib.compose.rippleClickable
|
||||
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.observeAsNonNullState
|
||||
import dev.patrickgold.florisboard.themeManager
|
||||
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
|
||||
|
||||
enum class ThemeManagerScreenAction(val id: String) {
|
||||
SELECT_DAY("select-day"),
|
||||
SELECT_NIGHT("select-night"),
|
||||
MANAGE("manage-installed-themes");
|
||||
SELECT_NIGHT("select-night");
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalJetPrefDatastoreUi::class)
|
||||
@Composable
|
||||
fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
title = stringRes(when (action) {
|
||||
ThemeManagerScreenAction.SELECT_DAY -> R.string.settings__theme_manager__title_day
|
||||
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")
|
||||
})
|
||||
previewFieldVisible = action != ThemeManagerScreenAction.MANAGE
|
||||
previewFieldVisible = true
|
||||
|
||||
val prefs by florisPreferenceModel()
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
val themeManager by context.themeManager()
|
||||
|
||||
val indexedThemeExtensions by extensionManager.themes.observeAsNonNullState()
|
||||
val selectedManagerThemeId = remember { mutableStateOf<ExtensionComponentName?>(null) }
|
||||
val extGroupedThemes = remember(indexedThemeExtensions) {
|
||||
buildMap<String, List<ThemeExtensionComponent>> {
|
||||
for (ext in indexedThemeExtensions) {
|
||||
@ -104,7 +77,6 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
fun getThemeIdPref() = when (action) {
|
||||
ThemeManagerScreenAction.SELECT_DAY -> prefs.theme.dayThemeId
|
||||
ThemeManagerScreenAction.SELECT_NIGHT -> prefs.theme.nightThemeId
|
||||
ThemeManagerScreenAction.MANAGE -> error("internal error in manager logic")
|
||||
}
|
||||
|
||||
fun setTheme(extId: String, componentId: String) {
|
||||
@ -114,18 +86,13 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
ThemeManagerScreenAction.SELECT_NIGHT -> {
|
||||
getThemeIdPref().set(extComponentName)
|
||||
}
|
||||
ThemeManagerScreenAction.MANAGE -> {
|
||||
selectedManagerThemeId.value = extComponentName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val activeThemeId by when (action) {
|
||||
ThemeManagerScreenAction.SELECT_DAY,
|
||||
ThemeManagerScreenAction.SELECT_NIGHT -> getThemeIdPref().observeAsState()
|
||||
ThemeManagerScreenAction.MANAGE -> selectedManagerThemeId
|
||||
}
|
||||
var themeExtToDelete by remember { mutableStateOf<Extension?>(null) }
|
||||
|
||||
content {
|
||||
DisposableEffect(activeThemeId) {
|
||||
@ -135,34 +102,12 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
val ext = extensionManager.getExtensionById(extensionId)!!
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = ext.meta.title,
|
||||
onTitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
||||
subtitle = extensionId,
|
||||
onSubtitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
||||
) {
|
||||
for (config in configs) key(extensionId, config.id) {
|
||||
JetPrefListItem(
|
||||
@ -171,8 +116,8 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
|
||||
},
|
||||
icon = {
|
||||
RadioButton(
|
||||
selected = activeThemeId?.extensionId == extensionId &&
|
||||
activeThemeId?.componentId == config.id,
|
||||
selected = activeThemeId.extensionId == extensionId &&
|
||||
activeThemeId.componentId == config.id,
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BrightnessAuto
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.FormatPaint
|
||||
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.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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.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.lib.android.launchUrl
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.themeManager
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun ThemeScreen() = FlorisScreen {
|
||||
@ -53,13 +50,21 @@ fun ThemeScreen() = FlorisScreen {
|
||||
|
||||
val context = LocalContext.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 {
|
||||
val themeMode by prefs.theme.mode.observeAsState()
|
||||
val dayThemeId by prefs.theme.dayThemeId.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)) {
|
||||
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 = {
|
||||
@ -68,7 +73,7 @@ fun ThemeScreen() = FlorisScreen {
|
||||
Text("Open Feedback Thread")
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
ListPreference(
|
||||
prefs.theme.mode,
|
||||
@ -85,53 +90,24 @@ fun ThemeScreen() = FlorisScreen {
|
||||
)
|
||||
}
|
||||
Preference(
|
||||
icon = Icons.Outlined.Palette,
|
||||
title = stringRes(R.string.settings__theme_manager__title_manage),
|
||||
icon = Icons.Default.LightMode,
|
||||
title = stringRes(R.string.pref__theme__day),
|
||||
summary = themeManager.getThemeLabel(dayThemeId),
|
||||
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_NIGHT },
|
||||
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(
|
||||
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 },
|
||||
)
|
||||
}
|
||||
AddonManagementReferenceBox(type = ExtensionListScreenType.EXT_THEME)
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,9 @@ package dev.patrickgold.florisboard.app.settings.typing
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.Language
|
||||
import androidx.compose.material.icons.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.SpaceBar
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Text
|
||||
@ -32,6 +32,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.ime.nlp.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||
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.ExperimentalJetPrefDatastoreUi
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||
|
||||
@ -51,6 +54,8 @@ fun TypingScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__typing__title)
|
||||
previewFieldVisible = true
|
||||
|
||||
val navController = LocalNavController.current
|
||||
|
||||
content {
|
||||
// This card is temporary and is therefore not using a string resource
|
||||
FlorisErrorCard(
|
||||
@ -159,12 +164,20 @@ fun TypingScreen() = FlorisScreen {
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.spelling.useUdmEntries,
|
||||
icon = Icons.Default.LibraryBooks,
|
||||
icon = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
title = stringRes(R.string.pref__spelling__use_udm_entries__label),
|
||||
summary = stringRes(R.string.pref__spelling__use_udm_entries__summary),
|
||||
enabledIf = { florisSpellCheckerEnabled.value },
|
||||
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.SentimentSatisfiedAlt
|
||||
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.Undo
|
||||
import androidx.compose.material.icons.outlined.Assignment
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
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.KeyType
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidInternalR
|
||||
import dev.patrickgold.jetpref.datastore.ui.vectorResource
|
||||
|
||||
interface ComputingEvaluator {
|
||||
@ -209,8 +208,7 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
|
||||
}
|
||||
KeyCode.COMPACT_LAYOUT_TO_LEFT,
|
||||
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> {
|
||||
// TODO: find a better icon for compact mode
|
||||
Icons.Default.Smartphone
|
||||
context()?.vectorResource(id = AndroidInternalR.drawable.ic_qs_one_handed_mode)
|
||||
}
|
||||
KeyCode.VOICE_INPUT -> {
|
||||
Icons.Default.KeyboardVoice
|
||||
@ -276,9 +274,9 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
|
||||
}
|
||||
KeyCode.TOGGLE_INCOGNITO_MODE -> {
|
||||
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 {
|
||||
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 -> {
|
||||
|
@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.keyboard
|
||||
import android.content.Context
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.appContext
|
||||
import dev.patrickgold.florisboard.assetManager
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
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.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.lib.io.ZipUtils
|
||||
import dev.patrickgold.florisboard.lib.io.loadJsonAsset
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -69,7 +69,6 @@ private data class CachedPopupMapping(
|
||||
class LayoutManager(context: Context) {
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val appContext by context.appContext()
|
||||
private val assetManager by context.assetManager()
|
||||
private val extensionManager by context.extensionManager()
|
||||
private val keyboardManager by context.keyboardManager()
|
||||
|
||||
@ -101,7 +100,7 @@ class LayoutManager(context: Context) {
|
||||
val layout = async {
|
||||
runCatching {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -128,7 +127,7 @@ class LayoutManager(context: Context) {
|
||||
val popupMapping = async {
|
||||
runCatching {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -51,4 +51,11 @@ object AndroidInternalR {
|
||||
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
|
||||
|
||||
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.FsDir
|
||||
import dev.patrickgold.florisboard.lib.io.FsFile
|
||||
@ -113,6 +115,37 @@ abstract class Extension {
|
||||
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 {
|
||||
var meta: ExtensionMeta
|
||||
val dependencies: MutableList<String>
|
||||
|
@ -19,9 +19,9 @@ package dev.patrickgold.florisboard.lib.ext
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.FileObserver
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.LiveData
|
||||
import dev.patrickgold.florisboard.appContext
|
||||
import dev.patrickgold.florisboard.assetManager
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||
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.FsFile
|
||||
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.observeAsNonNullState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -86,13 +91,17 @@ class ExtensionManager(context: Context) {
|
||||
}
|
||||
|
||||
private val appContext by context.appContext()
|
||||
private val assetManager by context.assetManager()
|
||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH)
|
||||
val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_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() {
|
||||
keyboardExtensions.init()
|
||||
themes.init()
|
||||
@ -142,7 +151,7 @@ class ExtensionManager(context: Context) {
|
||||
fun delete(ext: Extension) {
|
||||
check(canDelete(ext)) { "Cannot delete extension!" }
|
||||
ext.unload(appContext)
|
||||
assetManager.delete(ext.sourceRef!!)
|
||||
ext.sourceRef!!.delete(appContext)
|
||||
}
|
||||
|
||||
inner class ExtensionIndex<T : Extension>(
|
||||
@ -200,11 +209,11 @@ class ExtensionManager(context: Context) {
|
||||
|
||||
private fun indexAssetsModule(): List<T> {
|
||||
val list = mutableListOf<T>()
|
||||
assetManager.listDirs(assetsModuleRef).fold(
|
||||
assetsModuleRef.listDirs(appContext).fold(
|
||||
onSuccess = { extRefs ->
|
||||
for (extRef in extRefs) {
|
||||
val fileRef = extRef.subRef(ExtensionDefaults.MANIFEST_FILE_NAME)
|
||||
assetManager.loadJsonAsset(fileRef, serializer, ExtensionJsonConfig).fold(
|
||||
fileRef.loadJsonAsset(appContext, serializer, ExtensionJsonConfig).fold(
|
||||
onSuccess = { ext ->
|
||||
ext.sourceRef = extRef
|
||||
list.add(ext)
|
||||
@ -224,7 +233,7 @@ class ExtensionManager(context: Context) {
|
||||
|
||||
private fun indexInternalModule(): List<T> {
|
||||
val list = mutableListOf<T>()
|
||||
assetManager.listFiles(internalModuleRef).fold(
|
||||
internalModuleRef.listFiles(appContext).fold(
|
||||
onSuccess = { extRefs ->
|
||||
for (extRef in extRefs) {
|
||||
val fileRef = extRef.absoluteFile(appContext)
|
||||
@ -233,7 +242,7 @@ class ExtensionManager(context: Context) {
|
||||
}
|
||||
ZipUtils.readFileFromArchive(appContext, extRef, ExtensionDefaults.MANIFEST_FILE_NAME).fold(
|
||||
onSuccess = { metaStr ->
|
||||
assetManager.loadJsonAsset(metaStr, serializer, ExtensionJsonConfig).fold(
|
||||
loadJsonAsset(metaStr, serializer, ExtensionJsonConfig).fold(
|
||||
onSuccess = { ext ->
|
||||
ext.sourceRef = extRef
|
||||
list.add(ext)
|
||||
|
@ -101,4 +101,8 @@ data class ExtensionMeta(
|
||||
* Use an SPDX license expression if this extension has multiple licenses.
|
||||
*/
|
||||
val license: String,
|
||||
)
|
||||
) {
|
||||
fun getUpdateJsonPair(): String {
|
||||
return "\"${id}\":\"${version}\""
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package dev.patrickgold.florisboard.lib.ext
|
||||
|
||||
import androidx.core.text.trimmedLength
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.lib.ValidationRule
|
||||
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.validate
|
||||
|
||||
// TODO: (priority=medium)
|
||||
// make all strings available for localize
|
||||
object ExtensionValidation {
|
||||
private val MetaIdRegex = """^[a-z][a-z0-9_]*(\.[a-z0-9][a-z0-9_]*)*${'$'}""".toRegex()
|
||||
private val ComponentIdRegex = """^[a-z][a-z0-9_]*${'$'}""".toRegex()
|
||||
@ -38,9 +37,9 @@ object ExtensionValidation {
|
||||
forProperty = "id"
|
||||
validator { str ->
|
||||
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()
|
||||
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"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a version")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_version)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -61,7 +60,7 @@ object ExtensionValidation {
|
||||
forProperty = "title"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a title")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_title)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -73,7 +72,7 @@ object ExtensionValidation {
|
||||
validator { str ->
|
||||
val maintainers = str.lines().filter { it.isNotBlank() }
|
||||
when {
|
||||
maintainers.isEmpty() -> resultInvalid(error = "Please enter at least one valid maintainer")
|
||||
maintainers.isEmpty() -> resultInvalid(error = R.string.ext__validation__enter_maintainer)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -84,7 +83,7 @@ object ExtensionValidation {
|
||||
forProperty = "license"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a license identifier")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_license)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -95,8 +94,8 @@ object ExtensionValidation {
|
||||
forProperty = "id"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a component ID")
|
||||
!ComponentIdRegex.matches(str) -> resultInvalid(error = "Please enter a component ID matching $ComponentIdRegex")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_component_id)
|
||||
!ComponentIdRegex.matches(str) -> resultInvalid(error = R.string.ext__validation__error_component_id, "component_id_regex" to ComponentIdRegex)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -107,8 +106,8 @@ object ExtensionValidation {
|
||||
forProperty = "label"
|
||||
validator { str ->
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a component label")
|
||||
str.trimmedLength() > 30 -> resultValid(hint = "Your component label is quite long, which may lead to clipping in the UI")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_component_label)
|
||||
str.trimmedLength() > 30 -> resultValid(hint = R.string.ext__validation__hint_component_label_to_long)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -120,7 +119,7 @@ object ExtensionValidation {
|
||||
validator { str ->
|
||||
val authors = str.lines().filter { it.isNotBlank() }
|
||||
when {
|
||||
authors.isEmpty() -> resultInvalid(error = "Please enter at least one valid author")
|
||||
authors.isEmpty() -> resultInvalid(error = R.string.ext__validation__error_author)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -132,9 +131,9 @@ object ExtensionValidation {
|
||||
validator { str ->
|
||||
when {
|
||||
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) -> {
|
||||
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()
|
||||
}
|
||||
@ -147,12 +146,12 @@ object ExtensionValidation {
|
||||
validator { input ->
|
||||
val str = input.trim()
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a variable name")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_property)
|
||||
str == "-" || str.startsWith("--") -> resultValid()
|
||||
!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 ->
|
||||
val str = input.trim()
|
||||
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 -> {
|
||||
resultInvalid(error = "Please enter a valid color string")
|
||||
resultInvalid(error = R.string.ext__validation__error_color)
|
||||
}
|
||||
else -> resultValid()
|
||||
}
|
||||
@ -178,9 +177,9 @@ object ExtensionValidation {
|
||||
validator { str ->
|
||||
val floatValue = str.toFloatOrNull()
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a dp size")
|
||||
floatValue == null -> resultInvalid(error = "Please enter a valid number")
|
||||
floatValue < 0f -> resultInvalid(error = "Please enter a positive number (>=0)")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_dp_size)
|
||||
floatValue == null -> resultInvalid(error = R.string.ext__validation__enter_valid_number)
|
||||
floatValue < 0f -> resultInvalid(error = R.string.ext__validation__enter_positive_number)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
@ -192,10 +191,10 @@ object ExtensionValidation {
|
||||
validator { str ->
|
||||
val intValue = str.toIntOrNull()
|
||||
when {
|
||||
str.isBlank() -> resultInvalid(error = "Please enter a percent size")
|
||||
intValue == null -> resultInvalid(error = "Please enter a valid number")
|
||||
intValue < 0 || intValue > 100 -> resultInvalid(error = "Please enter a positive number between 0 and 100")
|
||||
intValue > 50 -> resultValid(hint = "Any value above 50% will behave as if you set 50%, consider lowering your percent size")
|
||||
str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_percent_size)
|
||||
intValue == null -> resultInvalid(error = R.string.ext__validation__enter_valid_number)
|
||||
intValue < 0 || intValue > 100 -> resultInvalid(error = R.string.ext__validation__enter_number_between_0_100)
|
||||
intValue > 50 -> resultValid(hint = R.string.ext__validation__hint_value_above_50_percent)
|
||||
else -> resultValid()
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.lib.io
|
||||
|
||||
import android.content.Context
|
||||
import dev.patrickgold.florisboard.appContext
|
||||
import dev.patrickgold.florisboard.ime.keyboard.AbstractKeyData
|
||||
import dev.patrickgold.florisboard.ime.keyboard.CaseSelector
|
||||
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
|
||||
// instances
|
||||
class AssetManager(context: Context) {
|
||||
val appContext by context.appContext()
|
||||
|
||||
fun delete(ref: FlorisRef) {
|
||||
when {
|
||||
ref.isCache || ref.isInternal -> {
|
||||
ref.absoluteFile(appContext).delete()
|
||||
}
|
||||
else -> error("Can not delete directory/file in location '${ref.scheme}'.")
|
||||
fun FlorisRef.delete(context: Context) {
|
||||
when {
|
||||
isCache || isInternal -> {
|
||||
absoluteFile(context).delete()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
else -> error("Can not delete directory/file in location '${scheme}'.")
|
||||
}
|
||||
}
|
||||
|
||||
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.net.Uri
|
||||
import dev.patrickgold.florisboard.assetManager
|
||||
import dev.patrickgold.florisboard.lib.android.copyRecursively
|
||||
import dev.patrickgold.florisboard.lib.android.write
|
||||
import java.io.FileOutputStream
|
||||
@ -28,10 +27,9 @@ import java.util.zip.ZipOutputStream
|
||||
|
||||
object ZipUtils {
|
||||
fun readFileFromArchive(context: Context, zipRef: FlorisRef, relPath: String) = runCatching<String> {
|
||||
val assetManager by context.assetManager()
|
||||
when {
|
||||
zipRef.isAssets -> {
|
||||
assetManager.loadTextAsset(zipRef.subRef(relPath)).getOrThrow()
|
||||
zipRef.subRef(relPath).loadTextAsset(context).getOrThrow()
|
||||
}
|
||||
zipRef.isCache || zipRef.isInternal -> {
|
||||
val flexHandle = FsFile(zipRef.absolutePath(context))
|
||||
@ -141,7 +139,18 @@ object ZipUtils {
|
||||
val flexEntries = flexFile.entries()
|
||||
while (flexEntries.hasMoreElements()) {
|
||||
val flexEntry = flexEntries.nextElement()
|
||||
if (flexEntry.name.length > 255) {
|
||||
continue
|
||||
}
|
||||
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) {
|
||||
flexEntryFile.mkdir()
|
||||
} else {
|
||||
@ -153,6 +162,9 @@ object ZipUtils {
|
||||
|
||||
private fun ZipFile.copy(srcEntry: ZipEntry, dstFile: FsFile) {
|
||||
dstFile.outputStream().use { outStream ->
|
||||
if (srcEntry.size > 100000000) {
|
||||
return
|
||||
}
|
||||
this.getInputStream(srcEntry).use { inStream ->
|
||||
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__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__any_theme__label" comment="Label of the theme selector preference">Selected theme</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="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__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_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_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__level">Editing level</string>
|
||||
@ -620,6 +617,10 @@
|
||||
|
||||
|
||||
<!-- 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__components">Bundled components</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_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__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 -->
|
||||
<string name="action__add">Add</string>
|
||||
|
Loading…
Reference in New Issue
Block a user