0
0
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:
Patrick Goldinger 2024-07-04 19:31:11 +02:00 committed by GitHub
commit c909d3ad7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 746 additions and 512 deletions

View File

@ -16,6 +16,8 @@ This includes, but is not exclusive to:
- Remove existing glide/swipe typing (see 0.5 milestone) - Remove existing glide/swipe typing (see 0.5 milestone)
- Improvements in clipboard / emoji functionality (v0.4.0-beta01/beta02) - Improvements in clipboard / emoji functionality (v0.4.0-beta01/beta02)
- Prepare project to have native code implemented in [Rust](https://www.rust-lang.org/) (v0.4.0-beta02) - Prepare project to have native code implemented in [Rust](https://www.rust-lang.org/) (v0.4.0-beta02)
- - Upgrade Settings UI to Material 3 (v0.4.0-beta03)
- Add support for importing extensions via system file handler APIs (relevant for Addons store) (v0.4.0-beta03)
Note that the previous versioning scheme has been dropped in favor of using a major.minor.patch versioning scheme, so versions like `0.3.16` are a thing of the past :) Note that the previous versioning scheme has been dropped in favor of using a major.minor.patch versioning scheme, so versions like `0.3.16` are a thing of the past :)
@ -32,7 +34,6 @@ Note that the previous versioning scheme has been dropped in favor of using a ma
- RFC document with technical details will be released later - RFC document with technical details will be released later
- Add Tablet mode / Optimizations for landscape input based on new keyboard layout engine - Add Tablet mode / Optimizations for landscape input based on new keyboard layout engine
- Reimplementation of glide typing with the new layout engine and predictive text core - Reimplementation of glide typing with the new layout engine and predictive text core
- Add support for importing extensions via system file handler APIs (relevant for Addons store)
- Add support for any remaining new features introduced with Android 13 - Add support for any remaining new features introduced with Android 13
## 0.6 ## 0.6
@ -52,7 +53,6 @@ Note that the previous versioning scheme has been dropped in favor of using a ma
**Features that MAY be added (even in versions mentioned above) or dismissed** **Features that MAY be added (even in versions mentioned above) or dismissed**
- Upgrade Settings UI to Material 3
- Full on-board layout editor which allows users to create their own layouts without writing a JSON file - Full on-board layout editor which allows users to create their own layouts without writing a JSON file
- Theme rework part II - Theme rework part II
- Adaptive themes v2 - Adaptive themes v2

View File

@ -64,6 +64,8 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BUILD_COMMIT_HASH", "\"${getGitCommitHash()}\"") buildConfigField("String", "BUILD_COMMIT_HASH", "\"${getGitCommitHash()}\"")
buildConfigField("String", "FLADDONS_API_VERSION", "\"v~draft2\"")
buildConfigField("String", "FLADDONS_STORE_URL", "\"fladdonstest.patrickgold.dev\"")
ksp { ksp {
arg("room.schemaLocation", "$projectDir/schemas") arg("room.schemaLocation", "$projectDir/schemas")

View File

@ -84,10 +84,23 @@
android:roundIcon="@mipmap/floris_app_icon_round" android:roundIcon="@mipmap/floris_app_icon_round"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:theme="@style/FlorisAppTheme.Splash" android:theme="@style/FlorisAppTheme.Splash"
android:exported="false"> android:exported="true">
<intent-filter> <intent-filter>
<data android:scheme="florisboard" android:host="app-ui"/> <data android:scheme="florisboard" android:host="app-ui"/>
</intent-filter> </intent-filter>
<intent-filter android:label="Import Extension">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="application/vnd.florisboard.extension+zip"/>
<data android:mimeType="application/octet-stream"/><!-- Firefox looking at you :eyes: -->
</intent-filter>
<intent-filter android:label="Import Extension">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/vnd.florisboard.extension+zip"/>
<data android:mimeType="application/octet-stream"/><!-- Firefox looking at you :eyes: -->
</intent-filter>
</activity> </activity>
<!-- Using an activity alias to disable/enable the app icon in the launcher --> <!-- Using an activity alias to disable/enable the app icon in the launcher -->
@ -106,24 +119,6 @@
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
<!-- Import File Bridging Activity -->
<activity
android:name="dev.patrickgold.florisboard.app.ext.ImportFileActivity"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:launchMode="singleTask"
android:roundIcon="@mipmap/floris_app_icon_round"
android:windowSoftInputMode="adjustResize"
android:theme="@style/FlorisAppTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="*" android:host="*" android:pathPattern=".*\\.flex"/>
<data android:scheme="*" android:host="*" android:pathPattern=".*\\.xpi"/>
</intent-filter>
</activity>
<!-- Crash Dialog Activity --> <!-- Crash Dialog Activity -->
<activity <activity
android:name="dev.patrickgold.florisboard.lib.crashutility.CrashDialogActivity" android:name="dev.patrickgold.florisboard.lib.crashutility.CrashDialogActivity"

View File

@ -40,7 +40,6 @@ import dev.patrickgold.florisboard.lib.devtools.Flog
import dev.patrickgold.florisboard.lib.devtools.LogTopic import dev.patrickgold.florisboard.lib.devtools.LogTopic
import dev.patrickgold.florisboard.lib.devtools.flogError import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.ext.ExtensionManager import dev.patrickgold.florisboard.lib.ext.ExtensionManager
import dev.patrickgold.florisboard.lib.io.AssetManager
import dev.patrickgold.florisboard.lib.io.deleteContentsRecursively import dev.patrickgold.florisboard.lib.io.deleteContentsRecursively
import dev.patrickgold.jetpref.datastore.JetPref import dev.patrickgold.jetpref.datastore.JetPref
import org.florisboard.lib.kotlin.tryOrNull import org.florisboard.lib.kotlin.tryOrNull
@ -67,7 +66,6 @@ class FlorisApplication : Application() {
private val prefs by florisPreferenceModel() private val prefs by florisPreferenceModel()
private val mainHandler by lazy { Handler(mainLooper) } private val mainHandler by lazy { Handler(mainLooper) }
val assetManager = lazy { AssetManager(this) }
val cacheManager = lazy { CacheManager(this) } val cacheManager = lazy { CacheManager(this) }
val clipboardManager = lazy { ClipboardManager(this) } val clipboardManager = lazy { ClipboardManager(this) }
val editorInstance = lazy { EditorInstance(this) } val editorInstance = lazy { EditorInstance(this) }
@ -144,8 +142,6 @@ private tailrec fun Context.florisApplication(): FlorisApplication {
fun Context.appContext() = lazyOf(this.florisApplication()) fun Context.appContext() = lazyOf(this.florisApplication())
fun Context.assetManager() = this.florisApplication().assetManager
fun Context.cacheManager() = this.florisApplication().cacheManager fun Context.cacheManager() = this.florisApplication().cacheManager
fun Context.clipboardManager() = this.florisApplication().clipboardManager fun Context.clipboardManager() = this.florisApplication().clipboardManager

View File

@ -642,19 +642,11 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "theme__mode", key = "theme__mode",
default = ThemeMode.FOLLOW_SYSTEM, default = ThemeMode.FOLLOW_SYSTEM,
) )
val dayThemeAdaptToApp = boolean(
key = "theme__day_theme_adapt_to_app",
default = false,
)
val dayThemeId = custom( val dayThemeId = custom(
key = "theme__day_theme_id", key = "theme__day_theme_id",
default = extCoreTheme("floris_day"), default = extCoreTheme("floris_day"),
serializer = ExtensionComponentName.Serializer, serializer = ExtensionComponentName.Serializer,
) )
val nightThemeAdaptToApp = boolean(
key = "theme__night_theme_adapt_to_app",
default = false,
)
val nightThemeId = custom( val nightThemeId = custom(
key = "theme__night_theme_id", key = "theme__night_theme_id",
default = extCoreTheme("floris_night"), default = extCoreTheme("floris_night"),

View File

@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.app package dev.patrickgold.florisboard.app
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@ -24,11 +25,11 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -41,7 +42,9 @@ import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import dev.patrickgold.florisboard.R import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.apptheme.FlorisAppTheme import dev.patrickgold.florisboard.app.apptheme.FlorisAppTheme
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
import dev.patrickgold.florisboard.app.setup.NotificationPermissionState import dev.patrickgold.florisboard.app.setup.NotificationPermissionState
import dev.patrickgold.florisboard.cacheManager
import dev.patrickgold.florisboard.lib.FlorisLocale import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.android.AndroidVersion import dev.patrickgold.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.android.hideAppIcon import dev.patrickgold.florisboard.lib.android.hideAppIcon
@ -70,9 +73,11 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
class FlorisAppActivity : ComponentActivity() { class FlorisAppActivity : ComponentActivity() {
private val prefs by florisPreferenceModel() private val prefs by florisPreferenceModel()
private val cacheManager by cacheManager()
private var appTheme by mutableStateOf(AppTheme.AUTO) private var appTheme by mutableStateOf(AppTheme.AUTO)
private var showAppIcon = true private var showAppIcon = true
private var resourcesContext by mutableStateOf(this as Context) private var resourcesContext by mutableStateOf(this as Context)
private var fileImportIntent by mutableStateOf<Intent?>(null)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// Splash screen should be installed before calling super.onCreate() // Splash screen should be installed before calling super.onCreate()
@ -110,14 +115,15 @@ class FlorisAppActivity : ComponentActivity() {
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs) AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
setContent { setContent {
ProvideLocalizedResources(resourcesContext) { ProvideLocalizedResources(resourcesContext) {
FlorisAppTheme(theme = appTheme, isMaterialYouAware = prefs.advanced.useMaterialYou.observeAsState().value) { val useMaterialYou by prefs.advanced.useMaterialYou.observeAsState()
FlorisAppTheme(theme = appTheme, isMaterialYouAware = useMaterialYou) {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
//SystemUiApp()
AppContent() AppContent()
} }
} }
} }
} }
onNewIntent(intent)
} }
} }
@ -135,6 +141,21 @@ class FlorisAppActivity : ComponentActivity() {
} }
} }
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
fileImportIntent = intent
return
}
if (intent?.action == Intent.ACTION_SEND && intent.clipData != null) {
fileImportIntent = intent
return
}
fileImportIntent = null
}
@Composable @Composable
private fun AppContent() { private fun AppContent() {
val navController = rememberNavController() val navController = rememberNavController()
@ -167,6 +188,20 @@ class FlorisAppActivity : ComponentActivity() {
} }
} }
LaunchedEffect(fileImportIntent) {
val intent = fileImportIntent
if (intent != null) {
val data = if (intent.action == Intent.ACTION_VIEW) {
intent.data!!
} else {
intent.clipData!!.getItemAt(0).uri
}
val workspace = runCatching { cacheManager.readFromUriIntoCache(data) }.getOrNull()
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, workspace?.uuid))
}
fileImportIntent = null
}
SideEffect { SideEffect {
navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher) navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher)
} }

View File

@ -27,8 +27,11 @@ import dev.patrickgold.florisboard.app.devtools.DevtoolsScreen
import dev.patrickgold.florisboard.app.devtools.ExportDebugLogScreen import dev.patrickgold.florisboard.app.devtools.ExportDebugLogScreen
import dev.patrickgold.florisboard.app.ext.ExtensionEditScreen import dev.patrickgold.florisboard.app.ext.ExtensionEditScreen
import dev.patrickgold.florisboard.app.ext.ExtensionExportScreen import dev.patrickgold.florisboard.app.ext.ExtensionExportScreen
import dev.patrickgold.florisboard.app.ext.ExtensionHomeScreen
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreen import dev.patrickgold.florisboard.app.ext.ExtensionImportScreen
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
import dev.patrickgold.florisboard.app.ext.ExtensionListScreen
import dev.patrickgold.florisboard.app.ext.ExtensionListScreenType
import dev.patrickgold.florisboard.app.ext.ExtensionViewScreen import dev.patrickgold.florisboard.app.ext.ExtensionViewScreen
import dev.patrickgold.florisboard.app.settings.HomeScreen import dev.patrickgold.florisboard.app.settings.HomeScreen
import dev.patrickgold.florisboard.app.settings.about.AboutScreen import dev.patrickgold.florisboard.app.settings.about.AboutScreen
@ -58,7 +61,7 @@ import dev.patrickgold.florisboard.app.settings.typing.TypingScreen
import dev.patrickgold.florisboard.app.setup.SetupScreen import dev.patrickgold.florisboard.app.setup.SetupScreen
import org.florisboard.lib.kotlin.curlyFormat import org.florisboard.lib.kotlin.curlyFormat
@Suppress("FunctionName") @Suppress("FunctionName", "ConstPropertyName")
object Routes { object Routes {
object Setup { object Setup {
const val Screen = "setup" const val Screen = "setup"
@ -117,6 +120,14 @@ object Routes {
} }
object Ext { object Ext {
const val Home = "ext"
const val List = "ext/list/{type}?showUpdate={showUpdate}"
fun List(
type: ExtensionListScreenType,
showUpdate: Boolean
) = List.curlyFormat("type" to type.id, "showUpdate" to showUpdate)
const val Edit = "ext/edit/{id}?create={serial_type}" const val Edit = "ext/edit/{id}?create={serial_type}"
fun Edit(id: String, serialType: String? = null): String { fun Edit(id: String, serialType: String? = null): String {
return Edit.curlyFormat("id" to id, "serial_type" to (serialType ?: "")) return Edit.curlyFormat("id" to id, "serial_type" to (serialType ?: ""))
@ -209,12 +220,20 @@ object Routes {
} }
composable(Devtools.ExportDebugLog) { ExportDebugLogScreen() } composable(Devtools.ExportDebugLog) { ExportDebugLogScreen() }
composable(Ext.Home) { ExtensionHomeScreen() }
composable(Ext.List) { navBackStack ->
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
ExtensionListScreenType.entries.firstOrNull { it.id == typeId }
} ?: error("unknown type")
val showUpdate = navBackStack.arguments?.getString("showUpdate")
ExtensionListScreen(type, showUpdate == "true")
}
composable(Ext.Edit) { navBackStack -> composable(Ext.Edit) { navBackStack ->
val extensionId = navBackStack.arguments?.getString("id") val extensionId = navBackStack.arguments?.getString("id")
val serialType = navBackStack.arguments?.getString("serial_type") val serialType = navBackStack.arguments?.getString("serial_type")
ExtensionEditScreen( ExtensionEditScreen(
id = extensionId.toString(), id = extensionId.toString(),
createSerialType = serialType.takeIf { it != null && it.isNotBlank() }, createSerialType = serialType.takeIf { !it.isNullOrBlank() },
) )
} }
composable(Ext.Export) { navBackStack -> composable(Ext.Export) { navBackStack ->

View File

@ -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),
),
)
}
}
}

View File

@ -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))
},
)
}
}
}

View File

@ -36,7 +36,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -100,12 +99,6 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
val cacheManager by context.cacheManager() val cacheManager by context.cacheManager()
val extensionManager by context.extensionManager() val extensionManager by context.extensionManager()
val initWsUuid by rememberSaveable { mutableStateOf(initUuid) }
var importResult by remember {
val workspace = initWsUuid?.let { cacheManager.importer.getWorkspaceByUuid(it) }?.let { resultOk(it) }
mutableStateOf(workspace)
}
fun getSkipReason(fileInfo: CacheManager.FileInfo): Int { fun getSkipReason(fileInfo: CacheManager.FileInfo): Int {
return when { return when {
!FileRegistry.matchesFileFilter(fileInfo, type.supportedFiles) -> { !FileRegistry.matchesFileFilter(fileInfo, type.supportedFiles) -> {
@ -119,29 +112,37 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
NATIVE_NULLPTR.toInt() NATIVE_NULLPTR.toInt()
} }
} }
fileInfo.mediaType == FileRegistry.FlexExtension.mediaType -> { else -> { // ext == null
R.string.ext__import__file_skip_ext_corrupted R.string.ext__import__file_skip_ext_corrupted
} }
else -> {
NATIVE_NULLPTR.toInt()
}
} }
} }
fun Result<CacheManager.ImporterWorkspace>.mapSkipReasons(): Result<CacheManager.ImporterWorkspace> {
return this.map { workspace ->
workspace.inputFileInfos.forEach { fileInfo ->
fileInfo.skipReason = getSkipReason(fileInfo)
}
workspace
}
}
var importResult by remember(initUuid) {
val workspace = initUuid?.let { cacheManager.importer.getWorkspaceByUuid(it) }
?.let { resultOk(it) }
?.mapSkipReasons()
mutableStateOf(workspace)
}
val importLauncher = rememberLauncherForActivityResult( val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents(), contract = ActivityResultContracts.GetMultipleContents(),
onResult = { uriList -> onResult = { uriList ->
// If uri is null it indicates that the selection activity // If uri is null it indicates that the selection activity
// was cancelled (mostly by pressing the back button), so // was cancelled (mostly by pressing the back button), so
// we don't display an error message here. // we don't display an error message here.
if (uriList.isNullOrEmpty()) return@rememberLauncherForActivityResult if (uriList.isEmpty()) return@rememberLauncherForActivityResult
importResult?.getOrNull()?.close() importResult?.getOrNull()?.close()
importResult = runCatching { cacheManager.readFromUriIntoCache(uriList) }.map { workspace -> importResult = runCatching { cacheManager.readFromUriIntoCache(uriList) }.mapSkipReasons()
workspace.inputFileInfos.forEach { fileInfo ->
fileInfo.skipReason = getSkipReason(fileInfo)
}
workspace
}
}, },
) )
@ -197,15 +198,17 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
} }
content { content {
FlorisOutlinedButton( if (initUuid == null) {
onClick = { FlorisOutlinedButton(
importLauncher.launch("*/*") onClick = {
}, importLauncher.launch("*/*")
modifier = Modifier },
.padding(vertical = 16.dp) modifier = Modifier
.align(Alignment.CenterHorizontally), .padding(vertical = 16.dp)
text = stringRes(R.string.action__select_files), .align(Alignment.CenterHorizontally),
) text = stringRes(R.string.action__select_files),
)
}
val result = importResult val result = importResult
when { when {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2021 Patrick Goldinger * Copyright (C) 2024 Patrick Goldinger
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,84 +16,135 @@
package dev.patrickgold.florisboard.app.ext package dev.patrickgold.florisboard.app.ext
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import dev.patrickgold.florisboard.R import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.stringRes import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.ExtensionManager
import dev.patrickgold.florisboard.lib.observeAsNonNullState
enum class ExtensionListScreenType(
val id: String,
@StringRes val titleResId: Int,
val getExtensionIndex: (ExtensionManager) -> ExtensionManager.ExtensionIndex<*>,
val launchExtensionCreate: ((NavController) -> Unit)?,
) {
EXT_THEME(
id = "ext-theme",
titleResId = R.string.ext__list__ext_theme,
getExtensionIndex = { it.themes },
launchExtensionCreate = { it.navigate(Routes.Ext.Edit("null", ThemeExtension.SERIAL_TYPE)) },
),
EXT_KEYBOARD(
id = "ext-keyboard",
titleResId = R.string.ext__list__ext_keyboard,
getExtensionIndex = { it.keyboardExtensions },
launchExtensionCreate = null,//{ it.navigate(Routes.Ext.Edit("null", KeyboardExtension.SERIAL_TYPE)) },
),
EXT_LANGUAGEPACK(
id = "ext-languagepack",
titleResId = R.string.ext__list__ext_languagepack,
getExtensionIndex = { it.languagePacks },
launchExtensionCreate = null,//{ it.navigate(Routes.Ext.Edit("null", LanguagePackExtension.SERIAL_TYPE)) },
);
}
@Composable @Composable
fun ExtensionListScreen() = FlorisScreen { fun ExtensionListScreen(type: ExtensionListScreenType, showUpdate: Boolean) = FlorisScreen {
title = stringRes(R.string.about__title) title = stringRes(type.titleResId)
previewFieldVisible = false
/*val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
val extensionManager = ExtensionManager.def val navController = LocalNavController.current
val extensionManager by context.extensionManager()
val extensionIndex by type.getExtensionIndex(extensionManager).observeAsNonNullState()
Column( content {
verticalArrangement = Arrangement.Top, if (showUpdate) {
horizontalAlignment = Alignment.CenterHorizontally, UpdateBox(extensionIndex = extensionIndex)
modifier = Modifier }
.fillMaxWidth() for (ext in extensionIndex) {
.padding(top = 24.dp, bottom = 32.dp) FlorisOutlinedBox(
) { modifier = Modifier.defaultFlorisOutlinedBox(),
FlorisAppIcon() title = ext.meta.title,
Text( subtitle = ext.meta.id,
text = stringRes(R.string.floris_app_name), ) {
fontSize = 24.sp, Text(
fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
modifier = Modifier.padding(top = 16.dp), text = ext.meta.description ?: "",
) style = MaterialTheme.typography.bodySmall,
} )
Preference( Row(
icon = R.drawable.ic_info, modifier = Modifier
title = stringRes(R.string.about__version__title), .fillMaxWidth()
summary = appVersion, .padding(horizontal = 6.dp),
onClick = { ) {
try { FlorisTextButton(
val isImeSelected = InputMethodUtils.checkIsFlorisboardSelected(context) onClick = {
if (isImeSelected) { navController.navigate(Routes.Ext.View(ext.meta.id))
FlorisClipboardManager.getInstance().addNewPlaintext(appVersion) },
} else { icon = Icons.Outlined.Info,
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager text = stringRes(id = R.string.ext__list__view_details),//stringRes(R.string.action__add),
val clip = ClipData.newPlainText("Florisboard version", appVersion) colors = ButtonDefaults.textButtonColors(),
clipboard.setPrimaryClip(clip) )
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Edit(ext.meta.id))
},
icon = Icons.Default.Edit,
text = stringRes(R.string.action__edit),
enabled = extensionManager.canDelete(ext),
)
} }
Toast.makeText(context, R.string.about__version_copied__title, Toast.LENGTH_SHORT).show()
} catch (e: Throwable) {
Toast.makeText(
context, context.getString(R.string.about__version_copied__error, e.message), Toast.LENGTH_SHORT
).show()
} }
}, }
) }
Preference(
icon = R.drawable.ic_history, if (type.launchExtensionCreate != null) {
title = stringRes(R.string.about__changelog__title), floatingActionButton {
summary = stringRes(R.string.about__changelog__summary), ExtendedFloatingActionButton(
onClick = { launchUrl(context, R.string.florisboard__changelog_url, arrayOf(BuildConfig.VERSION_NAME)) }, icon = {
) Icon(
Preference( imageVector = Icons.Default.Add,
icon = R.drawable.ic_code, contentDescription = stringRes(id = R.string.ext__editor__title_create_any),
title = stringRes(R.string.about__repository__title), )
summary = stringRes(R.string.about__repository__summary), },
onClick = { launchUrl(context, R.string.florisboard__repo_url) }, text = {
) Text(
Preference( text = stringRes(id = R.string.ext__editor__title_create_any),
icon = R.drawable.ic_policy, )
title = stringRes(R.string.about__privacy_policy__title), },
summary = stringRes(R.string.about__privacy_policy__summary), shape = FloatingActionButtonDefaults.extendedFabShape,
onClick = { launchUrl(context, R.string.florisboard__privacy_policy_url) }, onClick = { type.launchExtensionCreate.invoke(navController) },
) )
Preference( }
icon = R.drawable.ic_description, }
title = stringRes(R.string.about__project_license__title),
summary = stringRes(R.string.about__project_license__summary, "license_name" to "Apache 2.0"),
onClick = { navController.navigate(Routes.Settings.ProjectLicense) },
)
Preference(
icon = R.drawable.ic_description,
title = stringRes(id = R.string.about__third_party_licenses__title),
summary = stringRes(id = R.string.about__third_party_licenses__summary),
onClick = { navController.navigate(Routes.Settings.ThirdPartyLicenses) },
)*/
} }

View File

@ -18,10 +18,8 @@ package dev.patrickgold.florisboard.app.ext
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.Mail
import androidx.compose.material.icons.outlined.Mail import androidx.compose.material.icons.outlined.Mail
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -32,13 +30,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.lib.android.launchUrl import dev.patrickgold.florisboard.lib.android.launchUrl
import dev.patrickgold.florisboard.lib.compose.FlorisChip import dev.patrickgold.florisboard.lib.compose.FlorisChip
import dev.patrickgold.florisboard.lib.ext.ExtensionMaintainer import dev.patrickgold.florisboard.lib.ext.ExtensionMaintainer
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun ExtensionMaintainerChip( fun ExtensionMaintainerChip(
maintainer: ExtensionMaintainer, maintainer: ExtensionMaintainer,

View File

@ -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
}
}

View File

@ -18,14 +18,13 @@ package dev.patrickgold.florisboard.app.settings
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Adb import androidx.compose.material.icons.automirrored.outlined.Assignment
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.Gesture import androidx.compose.material.icons.filled.Gesture
import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.LibraryBooks
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
import androidx.compose.material.icons.filled.SmartButton import androidx.compose.material.icons.filled.SmartButton
import androidx.compose.material.icons.filled.Spellcheck import androidx.compose.material.icons.filled.Spellcheck
import androidx.compose.material.icons.outlined.Assignment
import androidx.compose.material.icons.outlined.Build import androidx.compose.material.icons.outlined.Build
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Keyboard import androidx.compose.material.icons.outlined.Keyboard
@ -131,18 +130,13 @@ fun HomeScreen() = FlorisScreen {
title = stringRes(R.string.settings__typing__title), title = stringRes(R.string.settings__typing__title),
onClick = { navController.navigate(Routes.Settings.Typing) }, onClick = { navController.navigate(Routes.Settings.Typing) },
) )
Preference(
icon = Icons.Default.LibraryBooks,
title = stringRes(R.string.settings__dictionary__title),
onClick = { navController.navigate(Routes.Settings.Dictionary) },
)
Preference( Preference(
icon = Icons.Default.Gesture, icon = Icons.Default.Gesture,
title = stringRes(R.string.settings__gestures__title), title = stringRes(R.string.settings__gestures__title),
onClick = { navController.navigate(Routes.Settings.Gestures) }, onClick = { navController.navigate(Routes.Settings.Gestures) },
) )
Preference( Preference(
icon = Icons.Outlined.Assignment, icon = Icons.AutoMirrored.Outlined.Assignment,
title = stringRes(R.string.settings__clipboard__title), title = stringRes(R.string.settings__clipboard__title),
onClick = { navController.navigate(Routes.Settings.Clipboard) }, onClick = { navController.navigate(Routes.Settings.Clipboard) },
) )
@ -152,9 +146,9 @@ fun HomeScreen() = FlorisScreen {
onClick = { navController.navigate(Routes.Settings.Media) }, onClick = { navController.navigate(Routes.Settings.Media) },
) )
Preference( Preference(
icon = Icons.Default.Adb, icon = Icons.Default.Extension,
title = stringRes(R.string.devtools__title), title = stringRes(R.string.ext__home__title),
onClick = { navController.navigate(Routes.Devtools.Home) }, onClick = { navController.navigate(Routes.Ext.Home) },
) )
Preference( Preference(
icon = Icons.Outlined.Build, icon = Icons.Outlined.Build,

View File

@ -28,12 +28,12 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.patrickgold.florisboard.R import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.assetManager
import dev.patrickgold.florisboard.lib.compose.FlorisScreen import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
import dev.patrickgold.florisboard.lib.compose.stringRes import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.io.FlorisRef import dev.patrickgold.florisboard.lib.io.FlorisRef
import dev.patrickgold.florisboard.lib.io.loadTextAsset
@Composable @Composable
fun ProjectLicenseScreen() = FlorisScreen { fun ProjectLicenseScreen() = FlorisScreen {
@ -41,7 +41,6 @@ fun ProjectLicenseScreen() = FlorisScreen {
scrollable = false scrollable = false
val context = LocalContext.current val context = LocalContext.current
val assetManager by context.assetManager()
content { content {
// Forcing LTR because the Apache 2.0 License shipped and displayed // Forcing LTR because the Apache 2.0 License shipped and displayed
@ -54,8 +53,8 @@ fun ProjectLicenseScreen() = FlorisScreen {
.florisVerticalScroll() .florisVerticalScroll()
.florisHorizontalScroll(), .florisHorizontalScroll(),
) { ) {
val licenseText = assetManager.loadTextAsset( val licenseText = FlorisRef.assets("license/project_license.txt").loadTextAsset(
FlorisRef.assets("license/project_license.txt") context
).getOrElse { ).getOrElse {
stringRes(R.string.about__project_license__error_license_text_failed, "error_message" to (it.message ?: "")) stringRes(R.string.about__project_license__error_license_text_failed, "error_message" to (it.message ?: ""))
} }

View File

@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.app.settings.advanced package dev.patrickgold.florisboard.app.settings.advanced
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Adb
import androidx.compose.material.icons.filled.Archive import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.FormatPaint import androidx.compose.material.icons.filled.FormatPaint
import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Language
@ -166,6 +167,11 @@ fun AdvancedScreen() = FlorisScreen {
title = stringRes(R.string.pref__advanced__incognito_mode__label), title = stringRes(R.string.pref__advanced__incognito_mode__label),
entries = IncognitoMode.listEntries(), entries = IncognitoMode.listEntries(),
) )
Preference(
icon = Icons.Default.Adb,
title = stringRes(R.string.devtools__title),
onClick = { navController.navigate(Routes.Devtools.Home) },
)
PreferenceGroup(title = stringRes(R.string.backup_and_restore__title)) { PreferenceGroup(title = stringRes(R.string.backup_and_restore__title)) {
Preference( Preference(

View File

@ -16,83 +16,56 @@
package dev.patrickgold.florisboard.app.settings.theme package dev.patrickgold.florisboard.app.settings.theme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Input
import androidx.compose.material.icons.filled.LightMode import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
import dev.patrickgold.florisboard.app.florisPreferenceModel import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.extensionManager import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
import dev.patrickgold.florisboard.lib.android.showLongToast
import dev.patrickgold.florisboard.lib.compose.FlorisConfirmDeleteDialog
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.rippleClickable import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.stringRes import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.Extension
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsNonNullState import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.themeManager import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.material.ui.JetPrefListItem import dev.patrickgold.jetpref.material.ui.JetPrefListItem
enum class ThemeManagerScreenAction(val id: String) { enum class ThemeManagerScreenAction(val id: String) {
SELECT_DAY("select-day"), SELECT_DAY("select-day"),
SELECT_NIGHT("select-night"), SELECT_NIGHT("select-night");
MANAGE("manage-installed-themes");
} }
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable @Composable
fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen { fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
title = stringRes(when (action) { title = stringRes(when (action) {
ThemeManagerScreenAction.SELECT_DAY -> R.string.settings__theme_manager__title_day ThemeManagerScreenAction.SELECT_DAY -> R.string.settings__theme_manager__title_day
ThemeManagerScreenAction.SELECT_NIGHT -> R.string.settings__theme_manager__title_night ThemeManagerScreenAction.SELECT_NIGHT -> R.string.settings__theme_manager__title_night
ThemeManagerScreenAction.MANAGE -> R.string.settings__theme_manager__title_manage
else -> error("Theme manager screen action must not be null") else -> error("Theme manager screen action must not be null")
}) })
previewFieldVisible = action != ThemeManagerScreenAction.MANAGE previewFieldVisible = true
val prefs by florisPreferenceModel() val prefs by florisPreferenceModel()
val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
val extensionManager by context.extensionManager() val extensionManager by context.extensionManager()
val themeManager by context.themeManager() val themeManager by context.themeManager()
val indexedThemeExtensions by extensionManager.themes.observeAsNonNullState() val indexedThemeExtensions by extensionManager.themes.observeAsNonNullState()
val selectedManagerThemeId = remember { mutableStateOf<ExtensionComponentName?>(null) }
val extGroupedThemes = remember(indexedThemeExtensions) { val extGroupedThemes = remember(indexedThemeExtensions) {
buildMap<String, List<ThemeExtensionComponent>> { buildMap<String, List<ThemeExtensionComponent>> {
for (ext in indexedThemeExtensions) { for (ext in indexedThemeExtensions) {
@ -104,7 +77,6 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
fun getThemeIdPref() = when (action) { fun getThemeIdPref() = when (action) {
ThemeManagerScreenAction.SELECT_DAY -> prefs.theme.dayThemeId ThemeManagerScreenAction.SELECT_DAY -> prefs.theme.dayThemeId
ThemeManagerScreenAction.SELECT_NIGHT -> prefs.theme.nightThemeId ThemeManagerScreenAction.SELECT_NIGHT -> prefs.theme.nightThemeId
ThemeManagerScreenAction.MANAGE -> error("internal error in manager logic")
} }
fun setTheme(extId: String, componentId: String) { fun setTheme(extId: String, componentId: String) {
@ -114,18 +86,13 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
ThemeManagerScreenAction.SELECT_NIGHT -> { ThemeManagerScreenAction.SELECT_NIGHT -> {
getThemeIdPref().set(extComponentName) getThemeIdPref().set(extComponentName)
} }
ThemeManagerScreenAction.MANAGE -> {
selectedManagerThemeId.value = extComponentName
}
} }
} }
val activeThemeId by when (action) { val activeThemeId by when (action) {
ThemeManagerScreenAction.SELECT_DAY, ThemeManagerScreenAction.SELECT_DAY,
ThemeManagerScreenAction.SELECT_NIGHT -> getThemeIdPref().observeAsState() ThemeManagerScreenAction.SELECT_NIGHT -> getThemeIdPref().observeAsState()
ThemeManagerScreenAction.MANAGE -> selectedManagerThemeId
} }
var themeExtToDelete by remember { mutableStateOf<Extension?>(null) }
content { content {
DisposableEffect(activeThemeId) { DisposableEffect(activeThemeId) {
@ -135,34 +102,12 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
} }
} }
val grayColor = LocalContentColor.current.copy(alpha = 0.56f) val grayColor = LocalContentColor.current.copy(alpha = 0.56f)
if (action == ThemeManagerScreenAction.MANAGE) {
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
this@content.Preference(
onClick = { navController.navigate(
Routes.Ext.Edit("null", ThemeExtension.SERIAL_TYPE)
) },
icon = Icons.Default.Add,
title = stringRes(R.string.ext__editor__title_create_theme),
)
this@content.Preference(
onClick = { navController.navigate(
Routes.Ext.Import(ExtensionImportScreenType.EXT_THEME, null)
) },
icon = Icons.Default.Input,
title = stringRes(R.string.action__import),
)
}
}
for ((extensionId, configs) in extGroupedThemes) key(extensionId) { for ((extensionId, configs) in extGroupedThemes) key(extensionId) {
val ext = extensionManager.getExtensionById(extensionId)!! val ext = extensionManager.getExtensionById(extensionId)!!
FlorisOutlinedBox( FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(), modifier = Modifier.defaultFlorisOutlinedBox(),
title = ext.meta.title, title = ext.meta.title,
onTitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
subtitle = extensionId, subtitle = extensionId,
onSubtitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
) { ) {
for (config in configs) key(extensionId, config.id) { for (config in configs) key(extensionId, config.id) {
JetPrefListItem( JetPrefListItem(
@ -171,8 +116,8 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
}, },
icon = { icon = {
RadioButton( RadioButton(
selected = activeThemeId?.extensionId == extensionId && selected = activeThemeId.extensionId == extensionId &&
activeThemeId?.componentId == config.id, activeThemeId.componentId == config.id,
onClick = null, onClick = null,
) )
}, },
@ -191,51 +136,7 @@ fun ThemeManagerScreen(action: ThemeManagerScreenAction?) = FlorisScreen {
}, },
) )
} }
if (action == ThemeManagerScreenAction.MANAGE && extensionManager.canDelete(ext)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
) {
FlorisTextButton(
onClick = {
themeExtToDelete = ext
},
icon = Icons.Default.Delete,
text = stringRes(R.string.action__delete),
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error,
),
)
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Edit(ext.meta.id))
},
icon = Icons.Default.Edit,
text = stringRes(R.string.action__edit),
)
}
}
} }
} }
if (themeExtToDelete != null) {
FlorisConfirmDeleteDialog(
onConfirm = {
runCatching {
extensionManager.delete(themeExtToDelete!!)
}.onFailure { error ->
context.showLongToast(
R.string.error__snackbar_message,
"error_message" to error.localizedMessage,
)
}
themeExtToDelete = null
},
onDismiss = { themeExtToDelete = null },
what = themeExtToDelete!!.meta.title,
)
}
} }
} }

View File

@ -16,35 +16,32 @@
package dev.patrickgold.florisboard.app.settings.theme package dev.patrickgold.florisboard.app.settings.theme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrightnessAuto import androidx.compose.material.icons.filled.BrightnessAuto
import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.FormatPaint
import androidx.compose.material.icons.filled.LightMode import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.ext.AddonManagementReferenceBox
import dev.patrickgold.florisboard.app.ext.ExtensionListScreenType
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeMode import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.lib.android.launchUrl
import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard
import dev.patrickgold.florisboard.lib.compose.FlorisScreen import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.stringRes import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ListPreference import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
@Composable @Composable
fun ThemeScreen() = FlorisScreen { fun ThemeScreen() = FlorisScreen {
@ -53,13 +50,21 @@ fun ThemeScreen() = FlorisScreen {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val themeManager by context.themeManager()
@Composable
fun ThemeManager.getThemeLabel(id: ExtensionComponentName): String {
val configs by indexedThemeConfigs.observeAsState()
configs?.get(id)?.let { return it.label }
return id.toString()
}
content { content {
val themeMode by prefs.theme.mode.observeAsState() val themeMode by prefs.theme.mode.observeAsState()
val dayThemeId by prefs.theme.dayThemeId.observeAsState() val dayThemeId by prefs.theme.dayThemeId.observeAsState()
val nightThemeId by prefs.theme.nightThemeId.observeAsState() val nightThemeId by prefs.theme.nightThemeId.observeAsState()
Card(modifier = Modifier.padding(8.dp)) { /*Card(modifier = Modifier.padding(8.dp)) {
Column(modifier = Modifier.padding(8.dp)) { Column(modifier = Modifier.padding(8.dp)) {
Text("If you want to give feedback on the new stylesheet editor and theme engine, please do so in below linked feedback thread:\n") Text("If you want to give feedback on the new stylesheet editor and theme engine, please do so in below linked feedback thread:\n")
Button(onClick = { Button(onClick = {
@ -68,7 +73,7 @@ fun ThemeScreen() = FlorisScreen {
Text("Open Feedback Thread") Text("Open Feedback Thread")
} }
} }
} }*/
ListPreference( ListPreference(
prefs.theme.mode, prefs.theme.mode,
@ -85,53 +90,24 @@ fun ThemeScreen() = FlorisScreen {
) )
} }
Preference( Preference(
icon = Icons.Outlined.Palette, icon = Icons.Default.LightMode,
title = stringRes(R.string.settings__theme_manager__title_manage), title = stringRes(R.string.pref__theme__day),
summary = themeManager.getThemeLabel(dayThemeId),
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_NIGHT },
onClick = { onClick = {
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.MANAGE)) navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_DAY))
},
)
Preference(
icon = Icons.Default.DarkMode,
title = stringRes(R.string.pref__theme__night),
summary = themeManager.getThemeLabel(nightThemeId),
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_DAY },
onClick = {
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_NIGHT))
}, },
) )
PreferenceGroup( AddonManagementReferenceBox(type = ExtensionListScreenType.EXT_THEME)
title = stringRes(R.string.pref__theme__day),
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_NIGHT },
) {
Preference(
icon = Icons.Default.LightMode,
title = stringRes(R.string.pref__theme__any_theme__label),
summary = dayThemeId.toString(),
onClick = {
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_DAY))
},
)
SwitchPreference(
prefs.theme.dayThemeAdaptToApp,
icon = Icons.Default.FormatPaint,
title = stringRes(R.string.pref__theme__any_theme_adapt_to_app__label),
summary = stringRes(R.string.pref__theme__any_theme_adapt_to_app__summary),
visibleIf = { false },
)
}
PreferenceGroup(
title = stringRes(R.string.pref__theme__night),
enabledIf = { prefs.theme.mode isNotEqualTo ThemeMode.ALWAYS_DAY },
) {
Preference(
icon = Icons.Default.DarkMode,
title = stringRes(R.string.pref__theme__any_theme__label),
summary = nightThemeId.toString(),
onClick = {
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_NIGHT))
},
)
SwitchPreference(
prefs.theme.nightThemeAdaptToApp,
icon = Icons.Default.FormatPaint,
title = stringRes(R.string.pref__theme__any_theme_adapt_to_app__label),
summary = stringRes(R.string.pref__theme__any_theme_adapt_to_app__summary),
visibleIf = { false },
)
}
} }
} }

View File

@ -19,9 +19,9 @@ package dev.patrickgold.florisboard.app.settings.typing
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.filled.Contacts import androidx.compose.material.icons.filled.Contacts
import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.LibraryBooks
import androidx.compose.material.icons.filled.SpaceBar import androidx.compose.material.icons.filled.SpaceBar
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -32,6 +32,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
import dev.patrickgold.florisboard.lib.android.AndroidVersion import dev.patrickgold.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
@ -42,6 +44,7 @@ import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
import dev.patrickgold.jetpref.datastore.ui.ListPreference import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
@ -51,6 +54,8 @@ fun TypingScreen() = FlorisScreen {
title = stringRes(R.string.settings__typing__title) title = stringRes(R.string.settings__typing__title)
previewFieldVisible = true previewFieldVisible = true
val navController = LocalNavController.current
content { content {
// This card is temporary and is therefore not using a string resource // This card is temporary and is therefore not using a string resource
FlorisErrorCard( FlorisErrorCard(
@ -159,12 +164,20 @@ fun TypingScreen() = FlorisScreen {
) )
SwitchPreference( SwitchPreference(
prefs.spelling.useUdmEntries, prefs.spelling.useUdmEntries,
icon = Icons.Default.LibraryBooks, icon = Icons.AutoMirrored.Filled.LibraryBooks,
title = stringRes(R.string.pref__spelling__use_udm_entries__label), title = stringRes(R.string.pref__spelling__use_udm_entries__label),
summary = stringRes(R.string.pref__spelling__use_udm_entries__summary), summary = stringRes(R.string.pref__spelling__use_udm_entries__summary),
enabledIf = { florisSpellCheckerEnabled.value }, enabledIf = { florisSpellCheckerEnabled.value },
visibleIf = { false }, // For now visibleIf = { false }, // For now
) )
} }
PreferenceGroup(title = stringRes(R.string.settings__dictionary__title)) {
Preference(
icon = Icons.AutoMirrored.Filled.LibraryBooks,
title = stringRes(R.string.settings__dictionary__title),
onClick = { navController.navigate(Routes.Settings.Dictionary) },
)
}
} }
} }

View File

@ -42,12 +42,10 @@ import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.material.icons.filled.SpaceBar import androidx.compose.material.icons.filled.SpaceBar
import androidx.compose.material.icons.filled.Undo import androidx.compose.material.icons.filled.Undo
import androidx.compose.material.icons.outlined.Assignment import androidx.compose.material.icons.outlined.Assignment
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import dev.patrickgold.florisboard.R import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.core.Subtype import dev.patrickgold.florisboard.ime.core.Subtype
@ -57,6 +55,7 @@ import dev.patrickgold.florisboard.ime.input.InputShiftState
import dev.patrickgold.florisboard.ime.text.key.KeyCode import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyType import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.lib.FlorisLocale import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.android.AndroidInternalR
import dev.patrickgold.jetpref.datastore.ui.vectorResource import dev.patrickgold.jetpref.datastore.ui.vectorResource
interface ComputingEvaluator { interface ComputingEvaluator {
@ -209,8 +208,7 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
} }
KeyCode.COMPACT_LAYOUT_TO_LEFT, KeyCode.COMPACT_LAYOUT_TO_LEFT,
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> { KeyCode.COMPACT_LAYOUT_TO_RIGHT -> {
// TODO: find a better icon for compact mode context()?.vectorResource(id = AndroidInternalR.drawable.ic_qs_one_handed_mode)
Icons.Default.Smartphone
} }
KeyCode.VOICE_INPUT -> { KeyCode.VOICE_INPUT -> {
Icons.Default.KeyboardVoice Icons.Default.KeyboardVoice
@ -276,9 +274,9 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
} }
KeyCode.TOGGLE_INCOGNITO_MODE -> { KeyCode.TOGGLE_INCOGNITO_MODE -> {
if (evaluator.state.isIncognitoMode) { if (evaluator.state.isIncognitoMode) {
ImageVector.vectorResource(theme = null, resId = R.drawable.ic_incognito, res = this.context()?.resources!!) this.context()?.vectorResource(id = R.drawable.ic_incognito)
} else { } else {
ImageVector.vectorResource(theme = null, resId = R.drawable.ic_incognito_off, res = this.context()?.resources!!) this.context()?.vectorResource(id = R.drawable.ic_incognito_off)
} }
} }
KeyCode.TOGGLE_AUTOCORRECT -> { KeyCode.TOGGLE_AUTOCORRECT -> {

View File

@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.keyboard
import android.content.Context import android.content.Context
import dev.patrickgold.florisboard.app.florisPreferenceModel import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.appContext import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.assetManager
import dev.patrickgold.florisboard.extensionManager import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.core.Subtype import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.popup.PopupMapping import dev.patrickgold.florisboard.ime.popup.PopupMapping
@ -34,6 +33,7 @@ import dev.patrickgold.florisboard.lib.devtools.flogDebug
import dev.patrickgold.florisboard.lib.devtools.flogWarning import dev.patrickgold.florisboard.lib.devtools.flogWarning
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.io.ZipUtils import dev.patrickgold.florisboard.lib.io.ZipUtils
import dev.patrickgold.florisboard.lib.io.loadJsonAsset
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -69,7 +69,6 @@ private data class CachedPopupMapping(
class LayoutManager(context: Context) { class LayoutManager(context: Context) {
private val prefs by florisPreferenceModel() private val prefs by florisPreferenceModel()
private val appContext by context.appContext() private val appContext by context.appContext()
private val assetManager by context.assetManager()
private val extensionManager by context.extensionManager() private val extensionManager by context.extensionManager()
private val keyboardManager by context.keyboardManager() private val keyboardManager by context.keyboardManager()
@ -101,7 +100,7 @@ class LayoutManager(context: Context) {
val layout = async { val layout = async {
runCatching { runCatching {
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow() val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
val arrangement = assetManager.loadJsonAsset<LayoutArrangement>(jsonStr).getOrThrow() val arrangement = loadJsonAsset<LayoutArrangement>(jsonStr).getOrThrow()
CachedLayout(ltn.type, ltn.name, meta, arrangement) CachedLayout(ltn.type, ltn.name, meta, arrangement)
} }
} }
@ -128,7 +127,7 @@ class LayoutManager(context: Context) {
val popupMapping = async { val popupMapping = async {
runCatching { runCatching {
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow() val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
val mapping = assetManager.loadJsonAsset<PopupMapping>(jsonStr).getOrThrow() val mapping = loadJsonAsset<PopupMapping>(jsonStr).getOrThrow()
CachedPopupMapping(name, meta, mapping) CachedPopupMapping(name, meta, mapping)
} }
} }

View File

@ -51,4 +51,11 @@ object AndroidInternalR {
Resources.getSystem().getIdentifier("ime_action_default", "string", "android") Resources.getSystem().getIdentifier("ime_action_default", "string", "android")
} }
} }
@SuppressLint("DiscouragedApi")
@Suppress("ClassName")
object drawable {
val ic_qs_one_handed_mode by lazy {
Resources.getSystem().getIdentifier("ic_qs_one_handed_mode", "drawable", "android")
}
}
} }

View File

@ -17,6 +17,8 @@
package dev.patrickgold.florisboard.lib.ext package dev.patrickgold.florisboard.lib.ext
import android.content.Context import android.content.Context
import android.net.Uri
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.lib.io.FlorisRef import dev.patrickgold.florisboard.lib.io.FlorisRef
import dev.patrickgold.florisboard.lib.io.FsDir import dev.patrickgold.florisboard.lib.io.FsDir
import dev.patrickgold.florisboard.lib.io.FsFile import dev.patrickgold.florisboard.lib.io.FsFile
@ -113,6 +115,37 @@ abstract class Extension {
abstract fun edit(): ExtensionEditor abstract fun edit(): ExtensionEditor
} }
/**
* Generates an update url for [Extension] lists.
*
* @param version the version of the api path
* @param host the host for the addons store
* @return the Url
*/
internal fun List<Extension>.generateUpdateUrl(
version: String = BuildConfig.FLADDONS_API_VERSION,
host: String = BuildConfig.FLADDONS_STORE_URL,
): String {
return Uri.Builder().run {
scheme("https")
authority(host)
appendPath("updates")
appendPath(version)
encodedFragment(
buildString {
append("data={")
for (extension in this@generateUpdateUrl) {
append(extension.meta.getUpdateJsonPair())
if (extension != this@generateUpdateUrl.last()) {
append(",")
}
}
append("}")
}
)
}.build().toString()
}
interface ExtensionEditor { interface ExtensionEditor {
var meta: ExtensionMeta var meta: ExtensionMeta
val dependencies: MutableList<String> val dependencies: MutableList<String>

View File

@ -19,9 +19,9 @@ package dev.patrickgold.florisboard.lib.ext
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.FileObserver import android.os.FileObserver
import androidx.compose.runtime.Composable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import dev.patrickgold.florisboard.appContext import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.assetManager
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.ime.text.composing.Appender import dev.patrickgold.florisboard.ime.text.composing.Appender
@ -37,7 +37,12 @@ import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.io.FlorisRef import dev.patrickgold.florisboard.lib.io.FlorisRef
import dev.patrickgold.florisboard.lib.io.FsFile import dev.patrickgold.florisboard.lib.io.FsFile
import dev.patrickgold.florisboard.lib.io.ZipUtils import dev.patrickgold.florisboard.lib.io.ZipUtils
import dev.patrickgold.florisboard.lib.io.delete
import dev.patrickgold.florisboard.lib.io.listDirs
import dev.patrickgold.florisboard.lib.io.listFiles
import dev.patrickgold.florisboard.lib.io.loadJsonAsset
import dev.patrickgold.florisboard.lib.io.writeJson import dev.patrickgold.florisboard.lib.io.writeJson
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -86,13 +91,17 @@ class ExtensionManager(context: Context) {
} }
private val appContext by context.appContext() private val appContext by context.appContext()
private val assetManager by context.assetManager()
private val ioScope = CoroutineScope(Dispatchers.IO) private val ioScope = CoroutineScope(Dispatchers.IO)
val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH) val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH)
val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_PATH) val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_PATH)
val languagePacks = ExtensionIndex(LanguagePackExtension.serializer(), IME_LANGUAGEPACK_PATH) val languagePacks = ExtensionIndex(LanguagePackExtension.serializer(), IME_LANGUAGEPACK_PATH)
@Composable
fun combinedExtensionList() = listOf(keyboardExtensions.observeAsNonNullState(), themes.observeAsNonNullState(), languagePacks.observeAsNonNullState()).map {
it.value
}.flatten()
fun init() { fun init() {
keyboardExtensions.init() keyboardExtensions.init()
themes.init() themes.init()
@ -142,7 +151,7 @@ class ExtensionManager(context: Context) {
fun delete(ext: Extension) { fun delete(ext: Extension) {
check(canDelete(ext)) { "Cannot delete extension!" } check(canDelete(ext)) { "Cannot delete extension!" }
ext.unload(appContext) ext.unload(appContext)
assetManager.delete(ext.sourceRef!!) ext.sourceRef!!.delete(appContext)
} }
inner class ExtensionIndex<T : Extension>( inner class ExtensionIndex<T : Extension>(
@ -200,11 +209,11 @@ class ExtensionManager(context: Context) {
private fun indexAssetsModule(): List<T> { private fun indexAssetsModule(): List<T> {
val list = mutableListOf<T>() val list = mutableListOf<T>()
assetManager.listDirs(assetsModuleRef).fold( assetsModuleRef.listDirs(appContext).fold(
onSuccess = { extRefs -> onSuccess = { extRefs ->
for (extRef in extRefs) { for (extRef in extRefs) {
val fileRef = extRef.subRef(ExtensionDefaults.MANIFEST_FILE_NAME) val fileRef = extRef.subRef(ExtensionDefaults.MANIFEST_FILE_NAME)
assetManager.loadJsonAsset(fileRef, serializer, ExtensionJsonConfig).fold( fileRef.loadJsonAsset(appContext, serializer, ExtensionJsonConfig).fold(
onSuccess = { ext -> onSuccess = { ext ->
ext.sourceRef = extRef ext.sourceRef = extRef
list.add(ext) list.add(ext)
@ -224,7 +233,7 @@ class ExtensionManager(context: Context) {
private fun indexInternalModule(): List<T> { private fun indexInternalModule(): List<T> {
val list = mutableListOf<T>() val list = mutableListOf<T>()
assetManager.listFiles(internalModuleRef).fold( internalModuleRef.listFiles(appContext).fold(
onSuccess = { extRefs -> onSuccess = { extRefs ->
for (extRef in extRefs) { for (extRef in extRefs) {
val fileRef = extRef.absoluteFile(appContext) val fileRef = extRef.absoluteFile(appContext)
@ -233,7 +242,7 @@ class ExtensionManager(context: Context) {
} }
ZipUtils.readFileFromArchive(appContext, extRef, ExtensionDefaults.MANIFEST_FILE_NAME).fold( ZipUtils.readFileFromArchive(appContext, extRef, ExtensionDefaults.MANIFEST_FILE_NAME).fold(
onSuccess = { metaStr -> onSuccess = { metaStr ->
assetManager.loadJsonAsset(metaStr, serializer, ExtensionJsonConfig).fold( loadJsonAsset(metaStr, serializer, ExtensionJsonConfig).fold(
onSuccess = { ext -> onSuccess = { ext ->
ext.sourceRef = extRef ext.sourceRef = extRef
list.add(ext) list.add(ext)

View File

@ -101,4 +101,8 @@ data class ExtensionMeta(
* Use an SPDX license expression if this extension has multiple licenses. * Use an SPDX license expression if this extension has multiple licenses.
*/ */
val license: String, val license: String,
) ) {
fun getUpdateJsonPair(): String {
return "\"${id}\":\"${version}\""
}
}

View File

@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.lib.ext package dev.patrickgold.florisboard.lib.ext
import androidx.core.text.trimmedLength import androidx.core.text.trimmedLength
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
import dev.patrickgold.florisboard.lib.ValidationRule import dev.patrickgold.florisboard.lib.ValidationRule
import dev.patrickgold.florisboard.lib.snygg.SnyggStylesheet import dev.patrickgold.florisboard.lib.snygg.SnyggStylesheet
@ -25,8 +26,6 @@ import dev.patrickgold.florisboard.lib.snygg.value.SnyggPercentShapeValue
import dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue import dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue
import dev.patrickgold.florisboard.lib.validate import dev.patrickgold.florisboard.lib.validate
// TODO: (priority=medium)
// make all strings available for localize
object ExtensionValidation { object ExtensionValidation {
private val MetaIdRegex = """^[a-z][a-z0-9_]*(\.[a-z0-9][a-z0-9_]*)*${'$'}""".toRegex() private val MetaIdRegex = """^[a-z][a-z0-9_]*(\.[a-z0-9][a-z0-9_]*)*${'$'}""".toRegex()
private val ComponentIdRegex = """^[a-z][a-z0-9_]*${'$'}""".toRegex() private val ComponentIdRegex = """^[a-z][a-z0-9_]*${'$'}""".toRegex()
@ -38,9 +37,9 @@ object ExtensionValidation {
forProperty = "id" forProperty = "id"
validator { str -> validator { str ->
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a package name") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_package_name)
MetaIdRegex.matches(str) -> resultValid() MetaIdRegex.matches(str) -> resultValid()
else -> resultInvalid("Package name does not match regex $MetaIdRegex") else -> resultInvalid(error = R.string.ext__validation__error_package_name, "id_regex" to MetaIdRegex)
} }
} }
} }
@ -50,7 +49,7 @@ object ExtensionValidation {
forProperty = "version" forProperty = "version"
validator { str -> validator { str ->
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a version") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_version)
else -> resultValid() else -> resultValid()
} }
} }
@ -61,7 +60,7 @@ object ExtensionValidation {
forProperty = "title" forProperty = "title"
validator { str -> validator { str ->
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a title") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_title)
else -> resultValid() else -> resultValid()
} }
} }
@ -73,7 +72,7 @@ object ExtensionValidation {
validator { str -> validator { str ->
val maintainers = str.lines().filter { it.isNotBlank() } val maintainers = str.lines().filter { it.isNotBlank() }
when { when {
maintainers.isEmpty() -> resultInvalid(error = "Please enter at least one valid maintainer") maintainers.isEmpty() -> resultInvalid(error = R.string.ext__validation__enter_maintainer)
else -> resultValid() else -> resultValid()
} }
} }
@ -84,7 +83,7 @@ object ExtensionValidation {
forProperty = "license" forProperty = "license"
validator { str -> validator { str ->
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a license identifier") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_license)
else -> resultValid() else -> resultValid()
} }
} }
@ -95,8 +94,8 @@ object ExtensionValidation {
forProperty = "id" forProperty = "id"
validator { str -> validator { str ->
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a component ID") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_component_id)
!ComponentIdRegex.matches(str) -> resultInvalid(error = "Please enter a component ID matching $ComponentIdRegex") !ComponentIdRegex.matches(str) -> resultInvalid(error = R.string.ext__validation__error_component_id, "component_id_regex" to ComponentIdRegex)
else -> resultValid() else -> resultValid()
} }
} }
@ -107,8 +106,8 @@ object ExtensionValidation {
forProperty = "label" forProperty = "label"
validator { str -> validator { str ->
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a component label") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_component_label)
str.trimmedLength() > 30 -> resultValid(hint = "Your component label is quite long, which may lead to clipping in the UI") str.trimmedLength() > 30 -> resultValid(hint = R.string.ext__validation__hint_component_label_to_long)
else -> resultValid() else -> resultValid()
} }
} }
@ -120,7 +119,7 @@ object ExtensionValidation {
validator { str -> validator { str ->
val authors = str.lines().filter { it.isNotBlank() } val authors = str.lines().filter { it.isNotBlank() }
when { when {
authors.isEmpty() -> resultInvalid(error = "Please enter at least one valid author") authors.isEmpty() -> resultInvalid(error = R.string.ext__validation__error_author)
else -> resultValid() else -> resultValid()
} }
} }
@ -132,9 +131,9 @@ object ExtensionValidation {
validator { str -> validator { str ->
when { when {
str.isEmpty() -> resultValid() str.isEmpty() -> resultValid()
str.isBlank() -> resultInvalid(error = "The stylesheet path must not be blank") str.isBlank() -> resultInvalid(error = R.string.ext__validation__error_stylesheet_path_blank)
!ThemeComponentStylesheetPathRegex.matches(str) -> { !ThemeComponentStylesheetPathRegex.matches(str) -> {
resultInvalid(error = "Please enter a valid stylesheet path matching $ThemeComponentStylesheetPathRegex") resultInvalid(error = R.string.ext__validation__error_stylesheet_path, "stylesheet_path_regex" to ThemeComponentStylesheetPathRegex)
} }
else -> resultValid() else -> resultValid()
} }
@ -147,12 +146,12 @@ object ExtensionValidation {
validator { input -> validator { input ->
val str = input.trim() val str = input.trim()
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a variable name") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_property)
str == "-" || str.startsWith("--") -> resultValid() str == "-" || str.startsWith("--") -> resultValid()
!ThemeComponentVariableNameRegex.matches(str) -> { !ThemeComponentVariableNameRegex.matches(str) -> {
resultInvalid(error = "Please enter a valid variable name matching $ThemeComponentVariableNameRegex") resultInvalid(error = R.string.ext__validation__error_property, "variable_name_regex" to ThemeComponentVariableNameRegex)
} }
else -> resultValid(hint = "By convention a FlorisCSS variable name starts with two dashes (--)") else -> resultValid(hint = R.string.ext__validation__hint_property)
} }
} }
} }
@ -163,9 +162,9 @@ object ExtensionValidation {
validator { input -> validator { input ->
val str = input.trim() val str = input.trim()
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a color string") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_color)
dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue.deserialize(str).isFailure -> { dev.patrickgold.florisboard.lib.snygg.value.SnyggSolidColorValue.deserialize(str).isFailure -> {
resultInvalid(error = "Please enter a valid color string") resultInvalid(error = R.string.ext__validation__error_color)
} }
else -> resultValid() else -> resultValid()
} }
@ -178,9 +177,9 @@ object ExtensionValidation {
validator { str -> validator { str ->
val floatValue = str.toFloatOrNull() val floatValue = str.toFloatOrNull()
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a dp size") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_dp_size)
floatValue == null -> resultInvalid(error = "Please enter a valid number") floatValue == null -> resultInvalid(error = R.string.ext__validation__enter_valid_number)
floatValue < 0f -> resultInvalid(error = "Please enter a positive number (>=0)") floatValue < 0f -> resultInvalid(error = R.string.ext__validation__enter_positive_number)
else -> resultValid() else -> resultValid()
} }
} }
@ -192,10 +191,10 @@ object ExtensionValidation {
validator { str -> validator { str ->
val intValue = str.toIntOrNull() val intValue = str.toIntOrNull()
when { when {
str.isBlank() -> resultInvalid(error = "Please enter a percent size") str.isBlank() -> resultInvalid(error = R.string.ext__validation__enter_percent_size)
intValue == null -> resultInvalid(error = "Please enter a valid number") intValue == null -> resultInvalid(error = R.string.ext__validation__enter_valid_number)
intValue < 0 || intValue > 100 -> resultInvalid(error = "Please enter a positive number between 0 and 100") intValue < 0 || intValue > 100 -> resultInvalid(error = R.string.ext__validation__enter_number_between_0_100)
intValue > 50 -> resultValid(hint = "Any value above 50% will behave as if you set 50%, consider lowering your percent size") intValue > 50 -> resultValid(hint = R.string.ext__validation__hint_value_above_50_percent)
else -> resultValid() else -> resultValid()
} }
} }

View File

@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.lib.io package dev.patrickgold.florisboard.lib.io
import android.content.Context import android.content.Context
import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.ime.keyboard.AbstractKeyData import dev.patrickgold.florisboard.ime.keyboard.AbstractKeyData
import dev.patrickgold.florisboard.ime.keyboard.CaseSelector import dev.patrickgold.florisboard.ime.keyboard.CaseSelector
import dev.patrickgold.florisboard.ime.keyboard.CharWidthSelector import dev.patrickgold.florisboard.ime.keyboard.CharWidthSelector
@ -66,122 +65,118 @@ val DefaultJsonConfig = Json {
} }
} }
// TODO: fully deprecate and remove this class, should be substituted by extension funs on the actual file and stream fun FlorisRef.delete(context: Context) {
// instances when {
class AssetManager(context: Context) { isCache || isInternal -> {
val appContext by context.appContext() absoluteFile(context).delete()
fun delete(ref: FlorisRef) {
when {
ref.isCache || ref.isInternal -> {
ref.absoluteFile(appContext).delete()
}
else -> error("Can not delete directory/file in location '${ref.scheme}'.")
} }
} else -> error("Can not delete directory/file in location '${scheme}'.")
fun hasAsset(ref: FlorisRef): Boolean {
return when {
ref.isAssets -> {
try {
val file = File(ref.relativePath)
val list = appContext.assets.list(file.parent?.toString() ?: "")
list?.contains(file.name) == true
} catch (e: Exception) {
false
}
}
ref.isCache || ref.isInternal -> {
val file = File(ref.absolutePath(appContext))
file.exists() && file.isFile
}
else -> false
}
}
fun list(ref: FlorisRef) = list(ref, files = true, dirs = true)
fun listFiles(ref: FlorisRef) = list(ref, files = true, dirs = false)
fun listDirs(ref: FlorisRef) = list(ref, files = false, dirs = true)
private fun list(ref: FlorisRef, files: Boolean, dirs: Boolean) = runCatching<List<FlorisRef>> {
when {
!files && !dirs -> listOf()
ref.isAssets -> {
appContext.assets.list(ref.relativePath)?.mapNotNull { fileName ->
val subList = appContext.assets.list("${ref.relativePath}/$fileName") ?: return@mapNotNull null
when {
files && dirs || files && subList.isEmpty() || dirs && subList.isNotEmpty() -> {
ref.subRef(fileName)
}
else -> null
}
} ?: listOf()
}
ref.isCache || ref.isInternal -> {
val dir = ref.absoluteFile(appContext)
if (dir.isDirectory) {
when {
files && dirs -> dir.listFiles()?.toList()
files -> dir.listFiles()?.filter { it.isFile }
dirs -> dir.listFiles()?.filter { it.isDirectory }
else -> null
}!!.map { ref.subRef(it.name) }
} else {
listOf()
}
}
else -> error("Unsupported FlorisRef source!")
}
}
fun <T> loadJsonAsset(
ref: FlorisRef,
serializer: KSerializer<T>,
jsonConfig: Json = DefaultJsonConfig,
) = runCatching<T> {
val jsonStr = loadTextAsset(ref).getOrThrow()
jsonConfig.decodeFromString(serializer, jsonStr)
}
inline fun <reified T> loadJsonAsset(jsonStr: String, jsonConfig: Json = DefaultJsonConfig): Result<T> {
return runCatching { jsonConfig.decodeFromString(jsonStr) }
}
fun <T> loadJsonAsset(
jsonStr: String,
serializer: KSerializer<T>,
jsonConfig: Json = DefaultJsonConfig,
) = runCatching<T> {
jsonConfig.decodeFromString(serializer, jsonStr)
}
fun loadTextAsset(ref: FlorisRef): Result<String> {
return when {
ref.isAssets -> runCatching {
appContext.assets.reader(ref.relativePath).use { it.readText() }
}
ref.isCache || ref.isInternal -> {
val file = File(ref.absolutePath(appContext))
val contents = readTextFile(file).getOrElse { return resultErr(it) }
if (contents.isBlank()) {
resultErrStr("File is blank!")
} else {
resultOk(contents)
}
}
else -> resultErrStr("Unsupported asset ref!")
}
}
/**
* Reads a given [file] and returns its content.
*
* @param file The file object.
* @return The contents of the file or an empty string, if the file does not exist.
*/
private fun readTextFile(file: File) = runCatching {
file.readText(Charsets.UTF_8)
} }
} }
fun FlorisRef.hasAsset(context: Context): Boolean {
return when {
isAssets -> {
try {
val file = File(relativePath)
val list = context.assets.list(file.parent?.toString() ?: "")
list?.contains(file.name) == true
} catch (e: Exception) {
false
}
}
isCache || isInternal -> {
val file = File(absolutePath(context))
file.exists() && file.isFile
}
else -> false
}
}
fun FlorisRef.list(context: Context) = list(context, files = true, dirs = true)
fun FlorisRef.listFiles(context: Context) = list(context, files = true, dirs = false)
fun FlorisRef.listDirs(context: Context) = list(context, files = false, dirs = true)
private fun FlorisRef.list(appContext: Context, files: Boolean, dirs: Boolean) = runCatching<List<FlorisRef>> {
when {
!files && !dirs -> listOf()
isAssets -> {
appContext.assets.list(relativePath)?.mapNotNull { fileName ->
val subList = appContext.assets.list("${relativePath}/$fileName") ?: return@mapNotNull null
when {
files && dirs || files && subList.isEmpty() || dirs && subList.isNotEmpty() -> {
subRef(fileName)
}
else -> null
}
} ?: listOf()
}
isCache || isInternal -> {
val dir = absoluteFile(appContext)
if (dir.isDirectory) {
when {
files && dirs -> dir.listFiles()?.toList()
files -> dir.listFiles()?.filter { it.isFile }
dirs -> dir.listFiles()?.filter { it.isDirectory }
else -> null
}!!.map { subRef(it.name) }
} else {
listOf()
}
}
else -> error("Unsupported FlorisRef source!")
}
}
fun <T> FlorisRef.loadJsonAsset(
context: Context,
serializer: KSerializer<T>,
jsonConfig: Json = DefaultJsonConfig,
) = runCatching<T> {
val jsonStr = loadTextAsset(context).getOrThrow()
jsonConfig.decodeFromString(serializer, jsonStr)
}
inline fun <reified T> loadJsonAsset(jsonStr: String, jsonConfig: Json = DefaultJsonConfig): Result<T> {
return runCatching { jsonConfig.decodeFromString(jsonStr) }
}
fun <T> loadJsonAsset(
jsonStr: String,
serializer: KSerializer<T>,
jsonConfig: Json = DefaultJsonConfig,
) = runCatching<T> {
jsonConfig.decodeFromString(serializer, jsonStr)
}
fun FlorisRef.loadTextAsset(context: Context): Result<String> {
return when {
isAssets -> runCatching {
context.assets.reader(relativePath).use { it.readText() }
}
isCache || isInternal -> {
val file = File(absolutePath(context))
val contents = readTextFile(file).getOrElse { return resultErr(it) }
if (contents.isBlank()) {
resultErrStr("File is blank!")
} else {
resultOk(contents)
}
}
else -> resultErrStr("Unsupported asset ref!")
}
}
/**
* Reads a given [file] and returns its content.
*
* @param file The file object.
* @return The contents of the file or an empty string, if the file does not exist.
*/
private fun readTextFile(file: File) = runCatching {
file.readText(Charsets.UTF_8)
}

View File

@ -18,7 +18,6 @@ package dev.patrickgold.florisboard.lib.io
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import dev.patrickgold.florisboard.assetManager
import dev.patrickgold.florisboard.lib.android.copyRecursively import dev.patrickgold.florisboard.lib.android.copyRecursively
import dev.patrickgold.florisboard.lib.android.write import dev.patrickgold.florisboard.lib.android.write
import java.io.FileOutputStream import java.io.FileOutputStream
@ -28,10 +27,9 @@ import java.util.zip.ZipOutputStream
object ZipUtils { object ZipUtils {
fun readFileFromArchive(context: Context, zipRef: FlorisRef, relPath: String) = runCatching<String> { fun readFileFromArchive(context: Context, zipRef: FlorisRef, relPath: String) = runCatching<String> {
val assetManager by context.assetManager()
when { when {
zipRef.isAssets -> { zipRef.isAssets -> {
assetManager.loadTextAsset(zipRef.subRef(relPath)).getOrThrow() zipRef.subRef(relPath).loadTextAsset(context).getOrThrow()
} }
zipRef.isCache || zipRef.isInternal -> { zipRef.isCache || zipRef.isInternal -> {
val flexHandle = FsFile(zipRef.absolutePath(context)) val flexHandle = FsFile(zipRef.absolutePath(context))
@ -141,7 +139,18 @@ object ZipUtils {
val flexEntries = flexFile.entries() val flexEntries = flexFile.entries()
while (flexEntries.hasMoreElements()) { while (flexEntries.hasMoreElements()) {
val flexEntry = flexEntries.nextElement() val flexEntry = flexEntries.nextElement()
if (flexEntry.name.length > 255) {
continue
}
val flexEntryFile = FsFile(dstDir, flexEntry.name) val flexEntryFile = FsFile(dstDir, flexEntry.name)
val canonicalDestinationDirPath = dstDir.canonicalPath
val canonicalDestinationFilePath = flexEntryFile.canonicalPath
if (canonicalDestinationFilePath.length > 1023) {
continue
}
if (!canonicalDestinationFilePath.startsWith(canonicalDestinationDirPath + FsFile.separator)) {
continue
}
if (flexEntry.isDirectory) { if (flexEntry.isDirectory) {
flexEntryFile.mkdir() flexEntryFile.mkdir()
} else { } else {
@ -153,6 +162,9 @@ object ZipUtils {
private fun ZipFile.copy(srcEntry: ZipEntry, dstFile: FsFile) { private fun ZipFile.copy(srcEntry: ZipEntry, dstFile: FsFile) {
dstFile.outputStream().use { outStream -> dstFile.outputStream().use { outStream ->
if (srcEntry.size > 100000000) {
return
}
this.getInputStream(srcEntry).use { inStream -> this.getInputStream(srcEntry).use { inStream ->
inStream.copyTo(outStream) inStream.copyTo(outStream)
} }

View File

@ -139,16 +139,13 @@
<string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Sunset time</string> <string name="pref__theme__sunset_time__label" comment="Label of the sunset time preference">Sunset time</string>
<string name="pref__theme__day" comment="Label of the day group (day means light theme)">Day theme</string> <string name="pref__theme__day" comment="Label of the day group (day means light theme)">Day theme</string>
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Night theme</string> <string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Night theme</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Selected theme</string> <string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Manage installed themes</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Adapt colors to app</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Theme colors adapt to those in the current app, if the target app supports this.</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">FlorisBoard App Assets</string> <string name="pref__theme__source_assets" comment="Label for the theme source field">FlorisBoard App Assets</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Internal Storage</string> <string name="pref__theme__source_internal" comment="Label for the theme source field">Internal Storage</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">External Provider</string> <string name="pref__theme__source_external" comment="Label for the theme source field">External Provider</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Select day theme</string> <string name="settings__theme_manager__title_day" comment="Title of the theme manager screen for day theme selection">Select day theme</string>
<string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Select night theme</string> <string name="settings__theme_manager__title_night" comment="Title of the theme manager screen for night theme selection">Select night theme</string>
<string name="settings__theme_manager__title_manage" comment="Title of the theme manager screen for managing installed and custom themes">Manage installed themes</string>
<string name="settings__theme_editor__fine_tune__title">Fine tune editor</string> <string name="settings__theme_editor__fine_tune__title">Fine tune editor</string>
<string name="settings__theme_editor__fine_tune__level">Editing level</string> <string name="settings__theme_editor__fine_tune__level">Editing level</string>
@ -620,6 +617,10 @@
<!-- Extension strings --> <!-- Extension strings -->
<string name="ext__home__title">Addons &amp; Extensions</string>
<string name="ext__list__ext_theme">Theme extensions</string>
<string name="ext__list__ext_keyboard">Keyboard extensions</string>
<string name="ext__list__ext_languagepack">Language pack extensions</string>
<string name="ext__meta__authors">Authors</string> <string name="ext__meta__authors">Authors</string>
<string name="ext__meta__components">Bundled components</string> <string name="ext__meta__components">Bundled components</string>
<string name="ext__meta__components_theme">Bundled themes</string> <string name="ext__meta__components_theme">Bundled themes</string>
@ -672,7 +673,39 @@
<string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">Expected a media file (image, audio, font, etc.) but found an extension archive.</string> <string name="ext__import__file_skip_ext_not_supported" comment="Reason string when file is loaded in incorrect context">Expected a media file (image, audio, font, etc.) but found an extension archive.</string>
<string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">Expected an extension archive but found a media file (image, audio, font, etc.).</string> <string name="ext__import__file_skip_media_not_supported" comment="Reason string when file is loaded in incorrect context">Expected an extension archive but found a media file (image, audio, font, etc.).</string>
<string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">An unexpected error occurred during import. The following details were provided:</string> <string name="ext__import__error_unexpected_exception" comment="Label when an error occurred during import. The error message will be appended below this text view">An unexpected error occurred during import. The following details were provided:</string>
<string name="ext__validation__enter_package_name">Please enter a package name</string>
<string name="ext__validation__error_package_name">Package name does not match regex {id_regex}</string>
<string name="ext__validation__enter_version">Please enter a version</string>
<string name="ext__validation__enter_title">Please enter a title</string>
<string name="ext__validation__enter_maintainer">Please enter at least one valid maintainer</string>
<string name="ext__validation__enter_license">Please enter a license identifier</string>
<string name="ext__validation__enter_component_id">Please enter a component ID</string>
<string name="ext__validation__error_component_id">Please enter a component ID matching {component_id_regex}</string>
<string name="ext__validation__enter_component_label">Please enter a component label</string>
<string name="ext__validation__hint_component_label_to_long">Your component label is quite long, which may lead to clipping in the UI</string>
<string name="ext__validation__error_author">Please enter at least one valid author</string>
<string name="ext__validation__error_stylesheet_path_blank">The stylesheet path must not be blank</string>
<string name="ext__validation__error_stylesheet_path">Please enter a valid stylesheet path matching {stylesheet_path_regex}</string>
<string name="ext__validation__enter_property">Please enter a variable name</string>
<string name="ext__validation__error_property">Please enter a valid variable name matching {variable_name_regex}</string>
<string name="ext__validation__hint_property" tools:ignore="TypographyDashes">By convention a FlorisCSS variable name starts with two dashes (--)</string>
<string name="ext__validation__enter_color">Please enter a color string</string>
<string name="ext__validation__error_color">Please enter a valid color string</string>
<string name="ext__validation__enter_dp_size">Please enter a dp size</string>
<string name="ext__validation__enter_valid_number">Please enter a valid number</string>
<string name="ext__validation__enter_positive_number">Please enter a positive number (>=0)</string>
<string name="ext__validation__enter_percent_size">Please enter a percent size</string>
<string name="ext__validation__enter_number_between_0_100">Please enter a positive number between 0 and 100</string>
<string name="ext__validation__hint_value_above_50_percent">Any value above 50% will behave as if you set 50%, consider lowering your percent size</string>
<string name="ext__update_box__internet_permission_hint">Since this app does not have Internet permission, updates for installed extensions must be checked manually.</string>
<string name="ext__update_box__search_for_updates">Search for Updates</string>
<string name="ext__addon_management_box__managing_placeholder">Managing {extensions}</string>
<string name="ext__addon_management_box__addon_manager_info">All tasks related to importing, exporting, creating, customizing, and removing extensions can be handled through the centralized addon manager.</string>
<string name="ext__addon_management_box__go_to_page">Go to {ext_home_title}</string>
<string name="ext__home__info">You can download and install extensions from the FlorisBoard Addons Store or import any extension file you have downloaded from the internet.</string>
<string name="ext__home__visit_store">Visit Addons Store</string>
<string name="ext__home__manage_extensions">Manage installed extensions</string>
<string name="ext__list__view_details">View details</string>
<!-- Action strings --> <!-- Action strings -->
<string name="action__add">Add</string> <string name="action__add">Add</string>