Add intent to import theme files directly

This commit is contained in:
MM20 2023-10-02 19:46:45 +02:00
parent e516848f4f
commit e83a6e8a31
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 462 additions and 4 deletions

View File

@ -25,7 +25,8 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
@ -62,9 +63,27 @@
android:value="de.mm20.launcher2.ui.launcher.SharedLauncherActivity" />
</activity>
<activity android:name=".launcher.sheets.BindAndConfigureAppWidgetActivity" />
<activity
android:name=".launcher.sheets.BindAndConfigureAppWidgetActivity"
/>
android:name=".launcher.ImportThemeActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@style/DialogTheme">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.de.mm20.launcher2.theme" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="*"
android:mimeType="*/*"
android:pathSuffix="kvtheme" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,310 @@
package de.mm20.launcher2.ui.common
import android.net.Uri
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DarkMode
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf
import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
import hct.Hct
@Composable
fun ImportThemeSheet(
uri: Uri,
onDismiss: () -> Unit,
) {
val viewModel: ImportThemeSheetVM = viewModel()
val context = LocalContext.current
LaunchedEffect(uri) {
viewModel.readTheme(context, uri)
}
val theme by viewModel.theme
val error by viewModel.error
var apply by viewModel.apply
BottomSheetDialog(
onDismissRequest = onDismiss,
confirmButton = if (theme != null && !error) {
{
Button(
onClick = {
viewModel.import()
onDismiss()
}
) {
Text(stringResource(R.string.action_import))
}
}
} else null,
) {
if (theme == null && !error) {
Box(
modifier = Modifier
.padding(it)
.fillMaxWidth()
.aspectRatio(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (error) {
Box(
modifier = Modifier
.padding(it)
.fillMaxWidth()
) {
LargeMessage(
icon = Icons.Rounded.ErrorOutline,
text = "Theme could not be read. Is the file corrupted?"
)
}
} else {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(it)
) {
ThemePreview(
theme!!,
)
Surface(
modifier = Modifier.padding(top = 8.dp),
color = MaterialTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.medium,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
) {
SwitchPreference(
title = stringResource(R.string.import_theme_apply),
iconPadding = false,
value = apply,
onValueChanged = {
apply = it
})
}
}
}
}
}
@Composable
fun ThemePreview(
theme: Theme,
modifier: Modifier = Modifier,
) {
val darkMode = LocalDarkTheme.current
var darkTheme by remember { mutableStateOf(darkMode) }
val colorScheme = if (darkTheme) darkColorSchemeOf(theme) else lightColorSchemeOf(theme)
Column(modifier = modifier) {
Row(
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = theme.name,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
SingleChoiceSegmentedButtonRow {
SegmentedButton(
shape = SegmentedButtonDefaults.shape(position = 0, count = 2),
selected = !darkTheme,
onClick = { darkTheme = false }
) {
Icon(Icons.Rounded.LightMode, null)
}
SegmentedButton(
shape = SegmentedButtonDefaults.shape(position = 1, count = 2),
selected = darkTheme,
onClick = { darkTheme = true }
) {
Icon(Icons.Rounded.DarkMode, null)
}
}
}
MaterialTheme(
colorScheme = colorScheme
) {
Column(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(start = 14.dp, end = 14.dp, bottom = 14.dp, top = 14.dp)
) {
Row {
ColorSwatch(color = MaterialTheme.colorScheme.primary, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.onPrimary, darkTheme = darkTheme)
ColorSwatch(
color = MaterialTheme.colorScheme.primaryContainer,
darkTheme = darkTheme
)
ColorSwatch(
color = MaterialTheme.colorScheme.onPrimaryContainer,
darkTheme = darkTheme
)
}
Row {
ColorSwatch(color = MaterialTheme.colorScheme.secondary, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.onSecondary, darkTheme = darkTheme)
ColorSwatch(
color = MaterialTheme.colorScheme.secondaryContainer,
darkTheme = darkTheme
)
ColorSwatch(
color = MaterialTheme.colorScheme.onSecondaryContainer,
darkTheme = darkTheme
)
}
Row {
ColorSwatch(color = MaterialTheme.colorScheme.tertiary, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.onTertiary, darkTheme = darkTheme)
ColorSwatch(
color = MaterialTheme.colorScheme.tertiaryContainer,
darkTheme = darkTheme
)
ColorSwatch(
color = MaterialTheme.colorScheme.onTertiaryContainer,
darkTheme = darkTheme
)
}
Row {
ColorSwatch(color = MaterialTheme.colorScheme.error, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.onError, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.errorContainer, darkTheme = darkTheme)
ColorSwatch(
color = MaterialTheme.colorScheme.onErrorContainer,
darkTheme = darkTheme
)
}
Row {
ColorSwatch(color = MaterialTheme.colorScheme.surfaceDim, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.surface, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.surfaceBright, darkTheme = darkTheme)
}
Row {
ColorSwatch(
color = MaterialTheme.colorScheme.surfaceContainerLowest,
darkTheme = darkTheme
)
ColorSwatch(
color = MaterialTheme.colorScheme.surfaceContainerLow,
darkTheme = darkTheme
)
ColorSwatch(
color = MaterialTheme.colorScheme.surfaceContainer,
darkTheme = darkTheme
)
ColorSwatch(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
darkTheme = darkTheme
)
ColorSwatch(
color = MaterialTheme.colorScheme.surfaceContainerHighest,
darkTheme = darkTheme
)
}
Row {
ColorSwatch(color = MaterialTheme.colorScheme.onSurface, darkTheme = darkTheme)
ColorSwatch(
color = MaterialTheme.colorScheme.onSurfaceVariant,
darkTheme = darkTheme
)
ColorSwatch(color = MaterialTheme.colorScheme.outline, darkTheme = darkTheme)
ColorSwatch(color = MaterialTheme.colorScheme.outlineVariant, darkTheme = darkTheme)
}
Row {
ColorSwatch(
color = MaterialTheme.colorScheme.inverseSurface,
darkTheme = darkTheme,
weight = 2f
)
ColorSwatch(
color = MaterialTheme.colorScheme.inverseOnSurface,
darkTheme = darkTheme
)
ColorSwatch(color = MaterialTheme.colorScheme.inversePrimary, darkTheme = darkTheme)
}
}
}
}
}
@Composable
fun RowScope.ColorSwatch(
color: Color,
darkTheme: Boolean,
weight: Float = 1f,
) {
val borderColor = Color(Hct.fromInt(color.toArgb()).let {
val tone = if (darkTheme) 30f else 80f
it.apply {
this.tone = tone.toDouble()
}.toInt()
})
Box(
modifier = Modifier
.weight(weight)
.padding(2.dp)
.height(36.dp)
.clip(MaterialTheme.shapes.small)
.border(
1.dp,
borderColor,
MaterialTheme.shapes.small
)
.background(color),
)
}

View File

@ -0,0 +1,71 @@
package de.mm20.launcher2.ui.common
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.themes.fromJson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ImportThemeSheetVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
private val dataStore: LauncherDataStore by inject()
val theme = mutableStateOf<Theme?>(null)
val error = mutableStateOf<Boolean>(false)
val apply = mutableStateOf<Boolean>(false)
fun import() {
val theme = theme.value
val apply = apply.value
if (theme != null) {
viewModelScope.launch {
importTheme(theme, apply)
}
}
}
fun readTheme(context: Context, uri: Uri) {
error.value = false
theme.value = null
apply.value = true
viewModelScope.launch(Dispatchers.IO) {
val inputStream =
context.contentResolver.openInputStream(uri) ?: return@launch
val theme = inputStream.use {
val json = it.readBytes().toString(Charsets.UTF_8)
try {
Theme.fromJson(json)
} catch (e: IllegalArgumentException) {
null
}
}
this@ImportThemeSheetVM.theme.value = theme
if (theme == null) {
error.value = true
}
}
}
private suspend fun importTheme(theme: Theme, apply: Boolean) {
themeRepository.createTheme(theme)
if (apply) {
dataStore.updateData {
it.toBuilder()
.setAppearance(
it.appearance.toBuilder()
.setThemeId(theme.id.toString())
).build()
}
}
}
}

View File

@ -0,0 +1,31 @@
package de.mm20.launcher2.ui.launcher
import android.os.Bundle
import androidx.activity.compose.setContent
import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.common.ImportThemeSheet
import de.mm20.launcher2.ui.overlays.OverlayHost
import de.mm20.launcher2.ui.theme.LauncherTheme
class ImportThemeActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data ?: return finish()
setContent {
LauncherTheme {
ProvideSettings {
OverlayHost {
ImportThemeSheet(
onDismiss = { finish() },
uri = uri,
)
}
}
}
}
}
}

View File

@ -31,4 +31,24 @@
<item name="windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<style name="DialogTheme" parent="BaseTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowContentOverlay">@null</item>
<item name="windowActionModeOverlay">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:statusBarColor">#00000000</item>
<item name="android:navigationBarColor">#00000000</item>
<item name="android:activityOpenEnterAnimation">@android:anim/fade_in</item>
<item name="android:activityCloseExitAnimation">@android:anim/fade_out</item>
</style>
</resources>

View File

@ -24,6 +24,7 @@
<!-- Shown in a snackbar after an item has been hidden. %1$s: label of the item -->
<string name="msg_item_hidden">%1$s has been hidden.</string>
<string name="action_undo">Undo</string>
<string name="action_import">Import</string>
<!-- Delete something (a file or a web search shortcut) -->
<string name="menu_delete">Delete</string>
<string name="menu_remove">Remove</string>
@ -839,4 +840,5 @@
<string name="theme_color_scheme_palette_color">Palette</string>
<string name="theme_color_scheme_custom_color">Custom</string>
<string name="preference_restore_default">Restore default</string>
<string name="import_theme_apply">Apply theme</string>
</resources>

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.themes
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
@ -53,5 +54,9 @@ fun Theme.toJson(): String {
}
fun Theme.Companion.fromJson(json: String): Theme {
return ThemeJson.decodeFromString(json)
return try {
ThemeJson.decodeFromString(json)
} catch (e: SerializationException) {
throw IllegalArgumentException(e)
}
}