Add intent to import theme files directly
This commit is contained in:
parent
e516848f4f
commit
e83a6e8a31
@ -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>
|
||||
@ -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),
|
||||
)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user