diff --git a/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt b/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt index 758af0423..283dc1fa1 100644 --- a/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt +++ b/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt @@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.core.content.pm.ShortcutInfoCompat @@ -29,21 +30,20 @@ import com.todoroo.andlib.utility.AndroidUtilities.atLeastS import kotlinx.coroutines.launch import org.tasks.R import org.tasks.analytics.Firebase +import org.tasks.billing.Inventory +import org.tasks.billing.PurchaseActivity import org.tasks.compose.DeleteButton import org.tasks.compose.IconPickerActivity.Companion.launchIconPicker import org.tasks.compose.IconPickerActivity.Companion.registerForIconPickerResult import org.tasks.compose.settings.ListSettingsContent import org.tasks.compose.settings.ListSettingsScaffold import org.tasks.data.UUIDHelper -import org.tasks.dialogs.ColorPalettePicker -import org.tasks.dialogs.ColorPalettePicker.Companion.newColorPalette -import org.tasks.dialogs.ColorPickerAdapter.Palette -import org.tasks.dialogs.ColorWheelPicker import org.tasks.extensions.addBackPressedCallback import org.tasks.filters.Filter import org.tasks.icons.OutlinedGoogleMaterial import org.tasks.intents.TaskIntents import org.tasks.preferences.DefaultFilterProvider +import org.tasks.themes.ColorProvider import org.tasks.themes.Theme import org.tasks.themes.contentColorFor import org.tasks.widget.RequestPinWidgetReceiver @@ -53,10 +53,12 @@ import org.tasks.widget.TasksWidget import javax.inject.Inject -abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicker.ColorPickedCallback, ColorWheelPicker.ColorPickedCallback { +abstract class BaseListSettingsActivity : AppCompatActivity() { @Inject lateinit var tasksTheme: Theme @Inject lateinit var defaultFilterProvider: DefaultFilterProvider @Inject lateinit var firebase: Firebase + @Inject lateinit var inventory: Inventory + @Inject lateinit var colorProvider: ColorProvider protected val baseViewModel: BaseListSettingsViewModel by viewModels() @@ -88,11 +90,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke } } - private fun showThemePicker() { - newColorPalette(null, 0, baseViewModel.color, Palette.COLORS) - .show(supportFragmentManager, FRAG_TAG_COLOR_PICKER) - } - private val launcher = registerForIconPickerResult { selected -> baseViewModel.setIcon(selected) } @@ -101,10 +98,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke launcher.launchIconPicker(this, baseViewModel.icon) } - override fun onColorPicked(color: Int) { - baseViewModel.setColor(color) - } - protected open fun promptDelete() { baseViewModel.promptDelete(true) } /** Standard @Compose view content for descendants. Caller must wrap it to TasksTheme{} */ @@ -132,7 +125,9 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke fab = fab, ) { ListSettingsContent( + hasPro = remember { inventory.purchasedThemes() }, color = viewState.color, + colors = remember { colorProvider.getThemeColors() }, icon = viewState.icon ?: defaultIcon, text = viewState.title, error = viewState.error, @@ -142,12 +137,16 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke baseViewModel.setTitle(it) baseViewModel.setError("") }, - pickColor = { showThemePicker() }, - clearColor = { onColorPicked(0) }, + setColor = { baseViewModel.setColor(it) }, pickIcon = { showIconPicker() }, addShortcutToHome = { createShortcut(color) }, addWidgetToHome = { createWidget() }, extensionContent = extensionContent, + purchase = { + startActivity( + Intent(this@BaseListSettingsActivity, PurchaseActivity::class.java) + ) + }, ) } } @@ -219,8 +218,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke } companion object { - private const val FRAG_TAG_COLOR_PICKER = "frag_tag_color_picker" - fun createShortcutIcon(context: Context, backgroundColor: Color): IconCompat { val size = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size) val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) diff --git a/app/src/main/java/org/tasks/compose/settings/ColorPicker.kt b/app/src/main/java/org/tasks/compose/settings/ColorPicker.kt new file mode 100644 index 000000000..7a32cd3eb --- /dev/null +++ b/app/src/main/java/org/tasks/compose/settings/ColorPicker.kt @@ -0,0 +1,130 @@ +package org.tasks.compose.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.tasks.themes.ThemeColor + +@Composable +fun ColorPicker( + hasPro: Boolean, + colors: List, + onSelected: (ThemeColor) -> Unit, + onColorWheelSelected: () -> Unit = {}, +) { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 48.dp), + contentPadding = PaddingValues(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth() + ) { + item { + ColorWheelCircle( + onClick = onColorWheelSelected, + hasPro = hasPro, + ) + } + items(colors) { color -> + ColorCircle( + color = color, + locked = !(hasPro || color.isFree), + onClick = { onSelected(color) } + ) + } + } +} + +@Composable +private fun ColorCircle( + color: ThemeColor, + locked: Boolean, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .aspectRatio(1f) + .clickable(onClick = onClick) + .size(48.dp) + .clip(CircleShape) + .background(Color(color.primaryColor)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = CircleShape + ), + contentAlignment = Alignment.Center, + ) { + if (locked) { + LockIcon(tint = Color(color.colorOnPrimary)) + } + } +} + +@Composable +private fun ColorWheelCircle( + onClick: () -> Unit, + hasPro: Boolean, +) { + Box( + modifier = Modifier + .aspectRatio(1f) + .clickable(onClick = onClick) + .size(48.dp) + .clip(CircleShape) + .background( + brush = Brush.sweepGradient( + colors = listOf( + Color.Red, + Color.Magenta, + Color.Blue, + Color.Cyan, + Color.Green, + Color.Yellow, + Color.Red + ) + ) + ) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + if (!hasPro) { + LockIcon(tint = Color.Black) + } + } +} + +@Composable +private fun LockIcon(tint: Color) { + Icon( + imageVector = Icons.Outlined.Lock, + contentDescription = null, + tint = tint, + modifier = Modifier.size(24.dp) + ) +} diff --git a/app/src/main/java/org/tasks/compose/settings/ColorPickerDialog.kt b/app/src/main/java/org/tasks/compose/settings/ColorPickerDialog.kt new file mode 100644 index 000000000..51d94d675 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/settings/ColorPickerDialog.kt @@ -0,0 +1,52 @@ +package org.tasks.compose.settings + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import org.tasks.themes.ThemeColor + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ColorPickerDialog( + hasPro: Boolean, + colors: List, + onDismiss: () -> Unit, + onColorSelected: (ThemeColor) -> Unit, + onColorWheelSelected: () -> Unit = {}, +) { + BasicAlertDialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surface, + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(16.dp) + ) { + Box(modifier = Modifier.padding(16.dp)) { + ColorPicker( + colors = colors, + onSelected = { color -> + onColorSelected(color) + onDismiss() + }, + onColorWheelSelected = { + onColorWheelSelected() + onDismiss() + }, + hasPro = hasPro, + ) + } + } + } +} diff --git a/app/src/main/java/org/tasks/compose/settings/ListSettingsContent.kt b/app/src/main/java/org/tasks/compose/settings/ListSettingsContent.kt index e1e35580d..3aa5a6e82 100644 --- a/app/src/main/java/org/tasks/compose/settings/ListSettingsContent.kt +++ b/app/src/main/java/org/tasks/compose/settings/ListSettingsContent.kt @@ -5,22 +5,25 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import org.tasks.compose.Constants +import org.tasks.themes.ThemeColor @Composable fun ColumnScope.ListSettingsContent( + hasPro: Boolean, color: Int, + colors: List, icon: String, text: String, error: String, requestKeyboard: Boolean, isNew: Boolean, setText: (String) -> Unit, - pickColor: () -> Unit, - clearColor: () -> Unit, + setColor: (Int) -> Unit, pickIcon: () -> Unit, addShortcutToHome: () -> Unit, addWidgetToHome: () -> Unit, extensionContent: @Composable ColumnScope.() -> Unit, + purchase: () -> Unit, ) { TitleInput( text = text, @@ -30,9 +33,11 @@ fun ColumnScope.ListSettingsContent( setText = { setText(it) }, ) SelectColorRow( + hasPro = hasPro, color = color, - selectColor = { pickColor() }, - clearColor = { clearColor() }, + colors = colors, + selectColor = { setColor(it) }, + purchase = { purchase() }, ) SelectIconRow( icon = icon, diff --git a/app/src/main/java/org/tasks/compose/settings/SelectColorRow.kt b/app/src/main/java/org/tasks/compose/settings/SelectColorRow.kt index 85ed65b37..d24f60dd7 100644 --- a/app/src/main/java/org/tasks/compose/settings/SelectColorRow.kt +++ b/app/src/main/java/org/tasks/compose/settings/SelectColorRow.kt @@ -1,5 +1,6 @@ package org.tasks.compose.settings +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Canvas import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -12,7 +13,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -22,26 +29,101 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.flask.colorpicker.ColorPickerView +import com.flask.colorpicker.builder.ColorPickerDialogBuilder import org.tasks.R import org.tasks.compose.Constants import org.tasks.kmp.org.tasks.compose.settings.SettingRow import org.tasks.themes.ColorProvider import org.tasks.themes.TasksTheme +import org.tasks.themes.ThemeColor @Composable fun SelectColorRow( + hasPro: Boolean, color: Int, - selectColor: () -> Unit, - clearColor: () -> Unit -) = + colors: List, + purchase: () -> Unit, + selectColor: (Int) -> Unit, +) { + var showColorPicker by rememberSaveable { mutableStateOf(false) } + var showColorWheel by rememberSaveable { mutableStateOf(false) } + if (showColorPicker) { + BackHandler { + showColorPicker = false + } + ColorPickerDialog( + hasPro = hasPro, + colors = colors, + onDismiss = { showColorPicker = false }, + onColorSelected = { + if (hasPro || it.isFree) { + selectColor(it.primaryColor) + } else { + purchase() + } + }, + onColorWheelSelected = { + if (hasPro) { + showColorWheel = true + } else { + purchase() + } + showColorPicker = false + }, + ) + } + if (showColorWheel) { + BackHandler { + showColorWheel = false + showColorPicker = true + } + val context = LocalContext.current + var selected by remember { mutableIntStateOf(0) } + LaunchedEffect(showColorWheel) { + if (!showColorWheel) return@LaunchedEffect + ColorPickerDialogBuilder + .with(context) + .wheelType(ColorPickerView.WHEEL_TYPE.CIRCLE) + .density(7) + .setOnColorChangedListener { which -> + selected = which + } + .setOnColorSelectedListener { which -> + selected = which + } + .lightnessSliderOnly() + .setPositiveButton(R.string.ok) { _, _, _ -> + selectColor(selected) + } + .setNegativeButton(R.string.cancel) { _, _ -> + showColorPicker = true + } + .apply { + if (color != 0) { + initialColor(color) + } + } + .build() + .apply { + setOnDismissListener { + showColorWheel = false + } + } + .show() + } + } SettingRow( - modifier = Modifier.clickable(onClick = selectColor), + modifier = Modifier.clickable(onClick = { showColorPicker = true }), left = { val context = LocalContext.current val adjusted = remember(color) { ColorProvider(context).getThemeColor(color).primaryColor } - IconButton(onClick = { selectColor() }) { + Box( + modifier = Modifier.size(56.dp), + contentAlignment = Alignment.Center, + ) { if (color == 0) { Icon( imageVector = Icons.Outlined.NotInterested, @@ -49,15 +131,11 @@ fun SelectColorRow( contentDescription = null ) } else { - val borderColor = colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary) - Box( - modifier = Modifier.size(56.dp), - contentAlignment = Alignment.Center - ) { - Canvas(modifier = Modifier.size(24.dp)) { - drawCircle(color = Color(adjusted)) - drawCircle(color = borderColor, style = Stroke(width = 4.0f)) - } + val borderColor = + colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary) + Canvas(modifier = Modifier.size(24.dp)) { + drawCircle(color = Color(adjusted)) + drawCircle(color = borderColor, style = Stroke(width = 4.0f)) } } } @@ -70,7 +148,7 @@ fun SelectColorRow( }, right = { if (color != 0) { - IconButton(onClick = clearColor) { + IconButton(onClick = { selectColor(0) }) { Icon( imageVector = Icons.Outlined.Clear, contentDescription = null @@ -79,15 +157,18 @@ fun SelectColorRow( } } ) +} @Composable @Preview(showBackground = true) private fun ColorSelectPreview () { TasksTheme { SelectColorRow( + hasPro = true, + colors = emptyList(), + purchase = {}, color = Color.Red.toArgb(), selectColor = {}, - clearColor = {} ) } }