Location: add icons for available payment methods (#1408)
* add backend for PaymentMethod.kt * change type of acceptedPaymentMethods to Map<PaymentMethod, Boolean> * add icons to UI and reduce PaymentMethod.kt to Cash and Card * make OSM parsing for acceptedPaymentMethods correct (in an opinionated way) * use Toll and TollOff icons * fix seperator on empty payment methods * fix sharedElement and animateEnterExit labels that are not part of the sharedElement * (feat) shape schemes --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,8 @@ import de.mm20.launcher2.database.entities.IconPackEntity
|
||||
import de.mm20.launcher2.database.entities.PluginEntity
|
||||
import de.mm20.launcher2.database.entities.SavedSearchableEntity
|
||||
import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||
import de.mm20.launcher2.database.entities.ThemeEntity
|
||||
import de.mm20.launcher2.database.entities.ColorsEntity
|
||||
import de.mm20.launcher2.database.entities.ShapesEntity
|
||||
import de.mm20.launcher2.database.entities.WidgetEntity
|
||||
import de.mm20.launcher2.database.migrations.Migration_10_11
|
||||
import de.mm20.launcher2.database.migrations.Migration_11_12
|
||||
@@ -37,6 +38,7 @@ import de.mm20.launcher2.database.migrations.Migration_23_24
|
||||
import de.mm20.launcher2.database.migrations.Migration_24_25
|
||||
import de.mm20.launcher2.database.migrations.Migration_25_26
|
||||
import de.mm20.launcher2.database.migrations.Migration_26_27
|
||||
import de.mm20.launcher2.database.migrations.Migration_27_28
|
||||
import de.mm20.launcher2.database.migrations.Migration_6_7
|
||||
import de.mm20.launcher2.database.migrations.Migration_7_8
|
||||
import de.mm20.launcher2.database.migrations.Migration_8_9
|
||||
@@ -54,9 +56,10 @@ import java.util.UUID
|
||||
WidgetEntity::class,
|
||||
CustomAttributeEntity::class,
|
||||
SearchActionEntity::class,
|
||||
ThemeEntity::class,
|
||||
ColorsEntity::class,
|
||||
PluginEntity::class,
|
||||
], version = 27, exportSchema = true
|
||||
ShapesEntity::class,
|
||||
], version = 28, exportSchema = true
|
||||
)
|
||||
@TypeConverters(ComponentNameConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -156,6 +159,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Migration_24_25(),
|
||||
Migration_25_26(),
|
||||
Migration_26_27(),
|
||||
Migration_27_28(),
|
||||
).build()
|
||||
if (_instance == null) _instance = instance
|
||||
return instance
|
||||
|
||||
@@ -4,30 +4,52 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import de.mm20.launcher2.database.entities.ThemeEntity
|
||||
import de.mm20.launcher2.database.entities.ColorsEntity
|
||||
import de.mm20.launcher2.database.entities.ShapesEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.UUID
|
||||
|
||||
@Dao
|
||||
interface ThemeDao {
|
||||
@Query("SELECT * FROM Theme")
|
||||
fun getAll(): Flow<List<ThemeEntity>>
|
||||
fun getAllColors(): Flow<List<ColorsEntity>>
|
||||
|
||||
@Query("SELECT * FROM Shapes")
|
||||
fun getAllShapes(): Flow<List<ShapesEntity>>
|
||||
|
||||
@Query("SELECT * FROM Theme WHERE id = :id LIMIT 1")
|
||||
fun get(id: UUID): Flow<ThemeEntity?>
|
||||
fun getColors(id: UUID): Flow<ColorsEntity?>
|
||||
|
||||
@Query("SELECT * FROM Shapes WHERE id = :id LIMIT 1")
|
||||
fun getShapes(id: UUID): Flow<ShapesEntity?>
|
||||
|
||||
@Insert
|
||||
suspend fun insert(theme: ThemeEntity)
|
||||
suspend fun insertColors(colors: ColorsEntity)
|
||||
|
||||
@Insert
|
||||
suspend fun insertShapes(shapes: ShapesEntity)
|
||||
|
||||
@Update
|
||||
suspend fun update(theme: ThemeEntity)
|
||||
suspend fun updateColors(colors: ColorsEntity)
|
||||
|
||||
@Update
|
||||
suspend fun updateShapes(shapes: ShapesEntity)
|
||||
|
||||
@Query("DELETE FROM Theme WHERE id = :id")
|
||||
suspend fun delete(id: UUID)
|
||||
suspend fun deleteColors(id: UUID)
|
||||
|
||||
@Query("DELETE FROM Shapes WHERE id = :id")
|
||||
suspend fun deleteShapes(id: UUID)
|
||||
|
||||
@Query("DELETE FROM Theme")
|
||||
suspend fun deleteAll()
|
||||
suspend fun deleteAllColors()
|
||||
|
||||
@Query("DELETE FROM Shapes")
|
||||
suspend fun deleteAllShapes()
|
||||
|
||||
@Insert
|
||||
fun insertAll(themes: List<ThemeEntity>)
|
||||
fun insertAllColors(colors: List<ColorsEntity>)
|
||||
|
||||
@Insert
|
||||
fun insertAllShapes(shapes: List<ShapesEntity>)
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(tableName = "Theme")
|
||||
data class ThemeEntity(
|
||||
data class ColorsEntity(
|
||||
@PrimaryKey val id: UUID,
|
||||
val name: String,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.mm20.launcher2.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(tableName = "Shapes")
|
||||
data class ShapesEntity(
|
||||
@PrimaryKey val id: UUID,
|
||||
val name: String,
|
||||
|
||||
val baseShape: String,
|
||||
|
||||
val extraSmall: String? = null,
|
||||
val small: String? = null,
|
||||
val medium: String? = null,
|
||||
val large: String? = null,
|
||||
val largeIncreased: String? = null,
|
||||
val extraLarge: String? = null,
|
||||
val extraLargeIncreased: String? = null,
|
||||
val extraExtraLarge: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.mm20.launcher2.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration_27_28: Migration(27, 28) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `Shapes` (
|
||||
`id` BLOB NOT NULL PRIMARY KEY,
|
||||
`name` TEXT NOT NULL,
|
||||
`baseShape` TEXT NOT NULL,
|
||||
`extraSmall` TEXT,
|
||||
`small` TEXT,
|
||||
`medium` TEXT,
|
||||
`large` TEXT,
|
||||
`largeIncreased` TEXT,
|
||||
`extraLarge` TEXT,
|
||||
`extraLargeIncreased` TEXT,
|
||||
`extraExtraLarge` TEXT
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.mm20.launcher2.locations
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.locations.providers.PluginLocation
|
||||
import de.mm20.launcher2.locations.providers.PluginLocationProvider
|
||||
import de.mm20.launcher2.locations.providers.openstreetmaps.OsmLocation
|
||||
@@ -18,10 +17,10 @@ import de.mm20.launcher2.search.location.Attribution
|
||||
import de.mm20.launcher2.search.location.Departure
|
||||
import de.mm20.launcher2.search.location.LocationIcon
|
||||
import de.mm20.launcher2.search.location.OpeningSchedule
|
||||
import de.mm20.launcher2.search.location.PaymentMethod
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
@Serializable
|
||||
internal data class SerializedLocation(
|
||||
@@ -42,6 +41,7 @@ internal data class SerializedLocation(
|
||||
val departures: List<Departure>? = null,
|
||||
val fixMeUrl: String? = null,
|
||||
val attribution: Attribution? = null,
|
||||
val acceptedPaymentMethods: Map<PaymentMethod, Boolean>? = null,
|
||||
val authority: String? = null,
|
||||
val storageStrategy: StorageStrategy? = null,
|
||||
)
|
||||
@@ -67,6 +67,7 @@ internal class OsmLocationSerializer : SearchableSerializer {
|
||||
timestamp = searchable.timestamp,
|
||||
departures = searchable.departures,
|
||||
fixMeUrl = searchable.fixMeUrl,
|
||||
acceptedPaymentMethods = searchable.acceptedPaymentMethods
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -96,6 +97,7 @@ internal class OsmLocationDeserializer(
|
||||
userRating = json.userRating,
|
||||
openingSchedule = json.openingSchedule,
|
||||
timestamp = json.timestamp ?: return null,
|
||||
acceptedPaymentMethods = json.acceptedPaymentMethods,
|
||||
updatedSelf = {
|
||||
osmProvider.update(id)
|
||||
}
|
||||
@@ -134,6 +136,7 @@ internal class PluginLocationSerializer : SearchableSerializer {
|
||||
openingSchedule = searchable.openingSchedule,
|
||||
timestamp = searchable.timestamp,
|
||||
departures = searchable.departures,
|
||||
acceptedPaymentMethods = searchable.acceptedPaymentMethods,
|
||||
fixMeUrl = searchable.fixMeUrl,
|
||||
authority = searchable.authority,
|
||||
storageStrategy = searchable.storageStrategy,
|
||||
@@ -185,6 +188,7 @@ internal class PluginLocationDeserializer(
|
||||
departures = json.departures,
|
||||
fixMeUrl = json.fixMeUrl,
|
||||
attribution = json.attribution,
|
||||
acceptedPaymentMethods = json.acceptedPaymentMethods,
|
||||
authority = authority,
|
||||
storageStrategy = strategy,
|
||||
updatedSelf = {
|
||||
|
||||
@@ -14,6 +14,7 @@ import de.mm20.launcher2.search.location.Attribution
|
||||
import de.mm20.launcher2.search.location.Departure
|
||||
import de.mm20.launcher2.search.location.LocationIcon
|
||||
import de.mm20.launcher2.search.location.OpeningSchedule
|
||||
import de.mm20.launcher2.search.location.PaymentMethod
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -34,6 +35,7 @@ data class PluginLocation(
|
||||
override val label: String,
|
||||
override val timestamp: Long,
|
||||
override val attribution: Attribution?,
|
||||
override val acceptedPaymentMethods: Map<PaymentMethod, Boolean>?,
|
||||
override val updatedSelf: (suspend (SavableSearchable) -> UpdateResult<Location>)?,
|
||||
override val labelOverride: String? = null,
|
||||
val authority: String,
|
||||
|
||||
+2
@@ -84,6 +84,7 @@ internal class PluginLocationProvider(
|
||||
userRatingCount = cursor[LocationColumns.UserRatingCount],
|
||||
departures = cursor[LocationColumns.Departures],
|
||||
attribution = cursor[LocationColumns.Attribution],
|
||||
acceptedPaymentMethods = cursor[LocationColumns.AcceptedPaymentMethods],
|
||||
authority = pluginAuthority,
|
||||
updatedSelf = {
|
||||
if (it !is PluginLocation) UpdateResult.TemporarilyUnavailable()
|
||||
@@ -117,6 +118,7 @@ internal class PluginLocationProvider(
|
||||
set(LocationColumns.UserRatingCount, userRatingCount)
|
||||
set(LocationColumns.Departures, departures)
|
||||
set(LocationColumns.Attribution, attribution)
|
||||
set(LocationColumns.AcceptedPaymentMethods, acceptedPaymentMethods)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-2
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import de.mm20.launcher2.ktx.ifNullOrEmpty
|
||||
import de.mm20.launcher2.ktx.into
|
||||
import de.mm20.launcher2.ktx.map
|
||||
import de.mm20.launcher2.ktx.stripStartOrNull
|
||||
import de.mm20.launcher2.locations.OsmLocationSerializer
|
||||
import de.mm20.launcher2.openstreetmaps.R
|
||||
import de.mm20.launcher2.search.Location
|
||||
@@ -16,6 +17,7 @@ import de.mm20.launcher2.search.location.Departure
|
||||
import de.mm20.launcher2.search.location.LocationIcon
|
||||
import de.mm20.launcher2.search.location.OpeningHours
|
||||
import de.mm20.launcher2.search.location.OpeningSchedule
|
||||
import de.mm20.launcher2.search.location.PaymentMethod
|
||||
import de.westnordost.osm_opening_hours.model.ClockTime
|
||||
import de.westnordost.osm_opening_hours.model.ExtendedClockTime
|
||||
import de.westnordost.osm_opening_hours.model.LastNth
|
||||
@@ -62,7 +64,8 @@ internal data class OsmLocation(
|
||||
override val labelOverride: String? = null,
|
||||
override val timestamp: Long,
|
||||
override var updatedSelf: (suspend (SavableSearchable) -> UpdateResult<Location>)? = null,
|
||||
override val userRating: Float?
|
||||
override val userRating: Float?,
|
||||
override val acceptedPaymentMethods: Map<PaymentMethod, Boolean>?
|
||||
) : Location, UpdatableSearchable<Location> {
|
||||
|
||||
override val domain: String
|
||||
@@ -115,7 +118,25 @@ internal data class OsmLocation(
|
||||
emailAddress = it.tags["email"] ?: it.tags["contact:email"],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
userRating = it.tags["stars"]?.runCatching { this.toInt() }?.getOrNull()
|
||||
?.let { min(it, 5) / 5.0f }
|
||||
?.let { min(it, 5) / 5.0f },
|
||||
acceptedPaymentMethods = with(
|
||||
it.tags.mapNotNull { (key, value) ->
|
||||
(key.stripStartOrNull("payment:") ?: return@mapNotNull null) to value
|
||||
}.toMap()
|
||||
) {
|
||||
// best-effort way to take any method payment as it being available,
|
||||
// otherwise as being unavailable, or undefined
|
||||
mapOf(
|
||||
PaymentMethod.Card to listOf("credit_cards", "debit_cards", "cards"),
|
||||
PaymentMethod.Cash to listOf("cash")
|
||||
).mapNotNull { (method, values) ->
|
||||
when {
|
||||
values.any { this[it] in listOf("yes", "only") } -> method to true
|
||||
values.any { this[it] == "no" } -> method to false
|
||||
else -> null
|
||||
}
|
||||
}.toMap().takeUnless { it.isEmpty() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+208
-202
@@ -1,138 +1,13 @@
|
||||
package de.mm20.launcher2.themes
|
||||
|
||||
import de.mm20.launcher2.database.entities.ThemeEntity
|
||||
import de.mm20.launcher2.database.entities.ColorsEntity
|
||||
import hct.Hct
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import java.util.UUID
|
||||
|
||||
enum class CorePaletteColor {
|
||||
Primary,
|
||||
Secondary,
|
||||
Tertiary,
|
||||
Neutral,
|
||||
NeutralVariant,
|
||||
Error;
|
||||
|
||||
override fun toString(): String {
|
||||
return when (this) {
|
||||
Primary -> "p"
|
||||
Secondary -> "s"
|
||||
Tertiary -> "t"
|
||||
Neutral -> "n"
|
||||
NeutralVariant -> "nv"
|
||||
Error -> "e"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun CorePaletteColor(color: String): CorePaletteColor? {
|
||||
return when (color) {
|
||||
"p" -> CorePaletteColor.Primary
|
||||
"s" -> CorePaletteColor.Secondary
|
||||
"t" -> CorePaletteColor.Tertiary
|
||||
"n" -> CorePaletteColor.Neutral
|
||||
"nv" -> CorePaletteColor.NeutralVariant
|
||||
"e" -> CorePaletteColor.Error
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Color
|
||||
|
||||
internal fun Color(string: String?): Color? {
|
||||
if (string == null) return null
|
||||
if (string.startsWith("#")) {
|
||||
return StaticColor(string.substring(1).toLongOrNull(16)?.toInt() ?: return null)
|
||||
}
|
||||
if (string.startsWith("$")) {
|
||||
val parts = string.substring(1).split(".").takeIf { it.size == 2 } ?: return null
|
||||
val color = CorePaletteColor(parts[0]) ?: return null
|
||||
return ColorRef(
|
||||
color = color,
|
||||
tone = parts[1].toIntOrNull() ?: return null,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
data class ColorRef(
|
||||
val color: CorePaletteColor,
|
||||
val tone: Int,
|
||||
) : Color {
|
||||
override fun toString(): String {
|
||||
return "\$$color.$tone"
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class StaticColor(val color: Int) : Color {
|
||||
override fun toString(): String {
|
||||
return "#${color.toUInt().toString(16).padStart(8, '0')}"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CorePalette<out T : Int?>(
|
||||
val primary: T,
|
||||
val secondary: T,
|
||||
val tertiary: T,
|
||||
val neutral: T,
|
||||
val neutralVariant: T,
|
||||
val error: T,
|
||||
)
|
||||
|
||||
val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
|
||||
|
||||
typealias FullCorePalette = CorePalette<Int>
|
||||
typealias PartialCorePalette = CorePalette<Int?>
|
||||
|
||||
@Serializable
|
||||
data class ColorScheme<out T : Color?>(
|
||||
val primary: T,
|
||||
val onPrimary: T,
|
||||
val primaryContainer: T,
|
||||
val onPrimaryContainer: T,
|
||||
val secondary: T,
|
||||
val onSecondary: T,
|
||||
val secondaryContainer: T,
|
||||
val onSecondaryContainer: T,
|
||||
val tertiary: T,
|
||||
val onTertiary: T,
|
||||
val tertiaryContainer: T,
|
||||
val onTertiaryContainer: T,
|
||||
val error: T,
|
||||
val onError: T,
|
||||
val errorContainer: T,
|
||||
val onErrorContainer: T,
|
||||
val surface: T,
|
||||
val onSurface: T,
|
||||
val onSurfaceVariant: T,
|
||||
val outline: T,
|
||||
val outlineVariant: T,
|
||||
val inverseSurface: T,
|
||||
val inverseOnSurface: T,
|
||||
val inversePrimary: T,
|
||||
val surfaceDim: T,
|
||||
val surfaceBright: T,
|
||||
val surfaceContainerLowest: T,
|
||||
val surfaceContainerLow: T,
|
||||
val surfaceContainer: T,
|
||||
val surfaceContainerHigh: T,
|
||||
val surfaceContainerHighest: T,
|
||||
|
||||
val background: T,
|
||||
val onBackground: T,
|
||||
val surfaceTint: T,
|
||||
val scrim: T,
|
||||
val surfaceVariant: T,
|
||||
)
|
||||
|
||||
typealias FullColorScheme = ColorScheme<Color>
|
||||
typealias PartialColorScheme = ColorScheme<Color?>
|
||||
|
||||
@Serializable
|
||||
data class Theme(
|
||||
data class Colors(
|
||||
@Transient val id: UUID = UUID.randomUUID(),
|
||||
val builtIn: Boolean = false,
|
||||
val name: String,
|
||||
@@ -141,7 +16,7 @@ data class Theme(
|
||||
val darkColorScheme: PartialColorScheme = DefaultDarkColorScheme,
|
||||
) {
|
||||
|
||||
constructor(entity: ThemeEntity) : this(
|
||||
constructor(entity: ColorsEntity) : this(
|
||||
id = entity.id,
|
||||
builtIn = false,
|
||||
name = entity.name,
|
||||
@@ -154,86 +29,86 @@ data class Theme(
|
||||
error = entity.corePaletteE,
|
||||
),
|
||||
lightColorScheme = ColorScheme(
|
||||
primary = Color(entity.lightPrimary),
|
||||
onPrimary = Color(entity.lightOnPrimary),
|
||||
primaryContainer = Color(entity.lightPrimaryContainer),
|
||||
onPrimaryContainer = Color(entity.lightOnPrimaryContainer),
|
||||
secondary = Color(entity.lightSecondary),
|
||||
onSecondary = Color(entity.lightOnSecondary),
|
||||
secondaryContainer = Color(entity.lightSecondaryContainer),
|
||||
onSecondaryContainer = Color(entity.lightOnSecondaryContainer),
|
||||
tertiary = Color(entity.lightTertiary),
|
||||
onTertiary = Color(entity.lightOnTertiary),
|
||||
tertiaryContainer = Color(entity.lightTertiaryContainer),
|
||||
onTertiaryContainer = Color(entity.lightOnTertiaryContainer),
|
||||
error = Color(entity.lightError),
|
||||
onError = Color(entity.lightOnError),
|
||||
errorContainer = Color(entity.lightErrorContainer),
|
||||
onErrorContainer = Color(entity.lightOnErrorContainer),
|
||||
surface = Color(entity.lightSurface),
|
||||
onSurface = Color(entity.lightOnSurface),
|
||||
onSurfaceVariant = Color(entity.lightOnSurfaceVariant),
|
||||
outline = Color(entity.lightOutline),
|
||||
outlineVariant = Color(entity.lightOutlineVariant),
|
||||
inverseSurface = Color(entity.lightInverseSurface),
|
||||
inverseOnSurface = Color(entity.lightInverseOnSurface),
|
||||
inversePrimary = Color(entity.lightInversePrimary),
|
||||
surfaceDim = Color(entity.lightSurfaceDim),
|
||||
surfaceBright = Color(entity.lightSurfaceBright),
|
||||
surfaceContainerLowest = Color(entity.lightSurfaceContainerLowest),
|
||||
surfaceContainerLow = Color(entity.lightSurfaceContainerLow),
|
||||
surfaceContainer = Color(entity.lightSurfaceContainer),
|
||||
surfaceContainerHigh = Color(entity.lightSurfaceContainerHigh),
|
||||
surfaceContainerHighest = Color(entity.lightSurfaceContainerHighest),
|
||||
background = Color(entity.lightBackground),
|
||||
onBackground = Color(entity.lightOnBackground),
|
||||
surfaceTint = Color(entity.lightSurfaceTint),
|
||||
scrim = Color(entity.lightScrim),
|
||||
surfaceVariant = Color(entity.lightSurfaceVariant),
|
||||
primary = Color.fromString(entity.lightPrimary),
|
||||
onPrimary = Color.fromString(entity.lightOnPrimary),
|
||||
primaryContainer = Color.fromString(entity.lightPrimaryContainer),
|
||||
onPrimaryContainer = Color.fromString(entity.lightOnPrimaryContainer),
|
||||
secondary = Color.fromString(entity.lightSecondary),
|
||||
onSecondary = Color.fromString(entity.lightOnSecondary),
|
||||
secondaryContainer = Color.fromString(entity.lightSecondaryContainer),
|
||||
onSecondaryContainer = Color.fromString(entity.lightOnSecondaryContainer),
|
||||
tertiary = Color.fromString(entity.lightTertiary),
|
||||
onTertiary = Color.fromString(entity.lightOnTertiary),
|
||||
tertiaryContainer = Color.fromString(entity.lightTertiaryContainer),
|
||||
onTertiaryContainer = Color.fromString(entity.lightOnTertiaryContainer),
|
||||
error = Color.fromString(entity.lightError),
|
||||
onError = Color.fromString(entity.lightOnError),
|
||||
errorContainer = Color.fromString(entity.lightErrorContainer),
|
||||
onErrorContainer = Color.fromString(entity.lightOnErrorContainer),
|
||||
surface = Color.fromString(entity.lightSurface),
|
||||
onSurface = Color.fromString(entity.lightOnSurface),
|
||||
onSurfaceVariant = Color.fromString(entity.lightOnSurfaceVariant),
|
||||
outline = Color.fromString(entity.lightOutline),
|
||||
outlineVariant = Color.fromString(entity.lightOutlineVariant),
|
||||
inverseSurface = Color.fromString(entity.lightInverseSurface),
|
||||
inverseOnSurface = Color.fromString(entity.lightInverseOnSurface),
|
||||
inversePrimary = Color.fromString(entity.lightInversePrimary),
|
||||
surfaceDim = Color.fromString(entity.lightSurfaceDim),
|
||||
surfaceBright = Color.fromString(entity.lightSurfaceBright),
|
||||
surfaceContainerLowest = Color.fromString(entity.lightSurfaceContainerLowest),
|
||||
surfaceContainerLow = Color.fromString(entity.lightSurfaceContainerLow),
|
||||
surfaceContainer = Color.fromString(entity.lightSurfaceContainer),
|
||||
surfaceContainerHigh = Color.fromString(entity.lightSurfaceContainerHigh),
|
||||
surfaceContainerHighest = Color.fromString(entity.lightSurfaceContainerHighest),
|
||||
background = Color.fromString(entity.lightBackground),
|
||||
onBackground = Color.fromString(entity.lightOnBackground),
|
||||
surfaceTint = Color.fromString(entity.lightSurfaceTint),
|
||||
scrim = Color.fromString(entity.lightScrim),
|
||||
surfaceVariant = Color.fromString(entity.lightSurfaceVariant),
|
||||
),
|
||||
darkColorScheme = ColorScheme(
|
||||
primary = Color(entity.darkPrimary),
|
||||
onPrimary = Color(entity.darkOnPrimary),
|
||||
primaryContainer = Color(entity.darkPrimaryContainer),
|
||||
onPrimaryContainer = Color(entity.darkOnPrimaryContainer),
|
||||
secondary = Color(entity.darkSecondary),
|
||||
onSecondary = Color(entity.darkOnSecondary),
|
||||
secondaryContainer = Color(entity.darkSecondaryContainer),
|
||||
onSecondaryContainer = Color(entity.darkOnSecondaryContainer),
|
||||
tertiary = Color(entity.darkTertiary),
|
||||
onTertiary = Color(entity.darkOnTertiary),
|
||||
tertiaryContainer = Color(entity.darkTertiaryContainer),
|
||||
onTertiaryContainer = Color(entity.darkOnTertiaryContainer),
|
||||
error = Color(entity.darkError),
|
||||
onError = Color(entity.darkOnError),
|
||||
errorContainer = Color(entity.darkErrorContainer),
|
||||
onErrorContainer = Color(entity.darkOnErrorContainer),
|
||||
surface = Color(entity.darkSurface),
|
||||
onSurface = Color(entity.darkOnSurface),
|
||||
onSurfaceVariant = Color(entity.darkOnSurfaceVariant),
|
||||
outline = Color(entity.darkOutline),
|
||||
outlineVariant = Color(entity.darkOutlineVariant),
|
||||
inverseSurface = Color(entity.darkInverseSurface),
|
||||
inverseOnSurface = Color(entity.darkInverseOnSurface),
|
||||
inversePrimary = Color(entity.darkInversePrimary),
|
||||
surfaceDim = Color(entity.darkSurfaceDim),
|
||||
surfaceBright = Color(entity.darkSurfaceBright),
|
||||
surfaceContainerLowest = Color(entity.darkSurfaceContainerLowest),
|
||||
surfaceContainerLow = Color(entity.darkSurfaceContainerLow),
|
||||
surfaceContainer = Color(entity.darkSurfaceContainer),
|
||||
surfaceContainerHigh = Color(entity.darkSurfaceContainerHigh),
|
||||
surfaceContainerHighest = Color(entity.darkSurfaceContainerHighest),
|
||||
background = Color(entity.darkBackground),
|
||||
onBackground = Color(entity.darkOnBackground),
|
||||
surfaceTint = Color(entity.darkSurfaceTint),
|
||||
scrim = Color(entity.darkScrim),
|
||||
surfaceVariant = Color(entity.darkSurfaceVariant),
|
||||
primary = Color.fromString(entity.darkPrimary),
|
||||
onPrimary = Color.fromString(entity.darkOnPrimary),
|
||||
primaryContainer = Color.fromString(entity.darkPrimaryContainer),
|
||||
onPrimaryContainer = Color.fromString(entity.darkOnPrimaryContainer),
|
||||
secondary = Color.fromString(entity.darkSecondary),
|
||||
onSecondary = Color.fromString(entity.darkOnSecondary),
|
||||
secondaryContainer = Color.fromString(entity.darkSecondaryContainer),
|
||||
onSecondaryContainer = Color.fromString(entity.darkOnSecondaryContainer),
|
||||
tertiary = Color.fromString(entity.darkTertiary),
|
||||
onTertiary = Color.fromString(entity.darkOnTertiary),
|
||||
tertiaryContainer = Color.fromString(entity.darkTertiaryContainer),
|
||||
onTertiaryContainer = Color.fromString(entity.darkOnTertiaryContainer),
|
||||
error = Color.fromString(entity.darkError),
|
||||
onError = Color.fromString(entity.darkOnError),
|
||||
errorContainer = Color.fromString(entity.darkErrorContainer),
|
||||
onErrorContainer = Color.fromString(entity.darkOnErrorContainer),
|
||||
surface = Color.fromString(entity.darkSurface),
|
||||
onSurface = Color.fromString(entity.darkOnSurface),
|
||||
onSurfaceVariant = Color.fromString(entity.darkOnSurfaceVariant),
|
||||
outline = Color.fromString(entity.darkOutline),
|
||||
outlineVariant = Color.fromString(entity.darkOutlineVariant),
|
||||
inverseSurface = Color.fromString(entity.darkInverseSurface),
|
||||
inverseOnSurface = Color.fromString(entity.darkInverseOnSurface),
|
||||
inversePrimary = Color.fromString(entity.darkInversePrimary),
|
||||
surfaceDim = Color.fromString(entity.darkSurfaceDim),
|
||||
surfaceBright = Color.fromString(entity.darkSurfaceBright),
|
||||
surfaceContainerLowest = Color.fromString(entity.darkSurfaceContainerLowest),
|
||||
surfaceContainerLow = Color.fromString(entity.darkSurfaceContainerLow),
|
||||
surfaceContainer = Color.fromString(entity.darkSurfaceContainer),
|
||||
surfaceContainerHigh = Color.fromString(entity.darkSurfaceContainerHigh),
|
||||
surfaceContainerHighest = Color.fromString(entity.darkSurfaceContainerHighest),
|
||||
background = Color.fromString(entity.darkBackground),
|
||||
onBackground = Color.fromString(entity.darkOnBackground),
|
||||
surfaceTint = Color.fromString(entity.darkSurfaceTint),
|
||||
scrim = Color.fromString(entity.darkScrim),
|
||||
surfaceVariant = Color.fromString(entity.darkSurfaceVariant),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
internal fun toEntity(): ThemeEntity {
|
||||
return ThemeEntity(
|
||||
internal fun toEntity(): ColorsEntity {
|
||||
return ColorsEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
corePaletteA1 = corePalette.primary,
|
||||
@@ -320,6 +195,137 @@ data class Theme(
|
||||
}
|
||||
}
|
||||
|
||||
enum class CorePaletteColor {
|
||||
Primary,
|
||||
Secondary,
|
||||
Tertiary,
|
||||
Neutral,
|
||||
NeutralVariant,
|
||||
Error;
|
||||
|
||||
override fun toString(): String {
|
||||
return when (this) {
|
||||
Primary -> "p"
|
||||
Secondary -> "s"
|
||||
Tertiary -> "t"
|
||||
Neutral -> "n"
|
||||
NeutralVariant -> "nv"
|
||||
Error -> "e"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): CorePaletteColor? {
|
||||
return when (string) {
|
||||
"p" -> Primary
|
||||
"s" -> Secondary
|
||||
"t" -> Tertiary
|
||||
"n" -> Neutral
|
||||
"nv" -> NeutralVariant
|
||||
"e" -> Error
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = ColorSerializer::class)
|
||||
sealed interface Color {
|
||||
companion object {
|
||||
fun fromString(string: String?): Color? {
|
||||
|
||||
if (string == null) return null
|
||||
if (string.startsWith("#")) {
|
||||
return StaticColor(string.substring(1).toLongOrNull(16)?.toInt() ?: return null)
|
||||
}
|
||||
if (string.startsWith("$")) {
|
||||
val parts = string.substring(1).split(".").takeIf { it.size == 2 } ?: return null
|
||||
val color = CorePaletteColor.fromString(parts[0]) ?: return null
|
||||
return ColorRef(
|
||||
color = color,
|
||||
tone = parts[1].toIntOrNull() ?: return null,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ColorRef(
|
||||
val color: CorePaletteColor,
|
||||
val tone: Int,
|
||||
) : Color {
|
||||
override fun toString(): String {
|
||||
return "$$color.$tone"
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class StaticColor(val color: Int) : Color {
|
||||
override fun toString(): String {
|
||||
return "#${color.toUInt().toString(16).padStart(8, '0')}"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CorePalette<out T : Int?>(
|
||||
val primary: T,
|
||||
val secondary: T,
|
||||
val tertiary: T,
|
||||
val neutral: T,
|
||||
val neutralVariant: T,
|
||||
val error: T,
|
||||
)
|
||||
|
||||
val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
|
||||
|
||||
typealias FullCorePalette = CorePalette<Int>
|
||||
typealias PartialCorePalette = CorePalette<Int?>
|
||||
|
||||
@Serializable
|
||||
data class ColorScheme<out T : Color?>(
|
||||
val primary: T,
|
||||
val onPrimary: T,
|
||||
val primaryContainer: T,
|
||||
val onPrimaryContainer: T,
|
||||
val secondary: T,
|
||||
val onSecondary: T,
|
||||
val secondaryContainer: T,
|
||||
val onSecondaryContainer: T,
|
||||
val tertiary: T,
|
||||
val onTertiary: T,
|
||||
val tertiaryContainer: T,
|
||||
val onTertiaryContainer: T,
|
||||
val error: T,
|
||||
val onError: T,
|
||||
val errorContainer: T,
|
||||
val onErrorContainer: T,
|
||||
val surface: T,
|
||||
val onSurface: T,
|
||||
val onSurfaceVariant: T,
|
||||
val outline: T,
|
||||
val outlineVariant: T,
|
||||
val inverseSurface: T,
|
||||
val inverseOnSurface: T,
|
||||
val inversePrimary: T,
|
||||
val surfaceDim: T,
|
||||
val surfaceBright: T,
|
||||
val surfaceContainerLowest: T,
|
||||
val surfaceContainerLow: T,
|
||||
val surfaceContainer: T,
|
||||
val surfaceContainerHigh: T,
|
||||
val surfaceContainerHighest: T,
|
||||
|
||||
val background: T,
|
||||
val onBackground: T,
|
||||
val surfaceTint: T,
|
||||
val scrim: T,
|
||||
val surfaceVariant: T,
|
||||
)
|
||||
|
||||
typealias FullColorScheme = ColorScheme<Color>
|
||||
typealias PartialColorScheme = ColorScheme<Color?>
|
||||
|
||||
fun <T : Int?> CorePalette<T>.get(color: CorePaletteColor): T {
|
||||
return when (color) {
|
||||
CorePaletteColor.Primary -> primary
|
||||
@@ -4,6 +4,10 @@ import java.util.UUID
|
||||
|
||||
|
||||
val DefaultThemeId = UUID(0L, 0L)
|
||||
val BlackAndWhiteThemeId = UUID(0L, 1L)
|
||||
val ExtraRoundShapesId = UUID(0L, 1L)
|
||||
val CutShapesId = UUID(0L, 2L)
|
||||
val RectShapesId = UUID(0L, 3L)
|
||||
|
||||
val DefaultLightColorScheme = ColorScheme<Color>(
|
||||
primary = ColorRef(CorePaletteColor.Primary, 40),
|
||||
@@ -83,7 +87,6 @@ val DefaultDarkColorScheme = ColorScheme<Color>(
|
||||
scrim = ColorRef(CorePaletteColor.Neutral, 0),
|
||||
)
|
||||
|
||||
val BlackAndWhiteThemeId = UUID(0L, 1L)
|
||||
|
||||
val BlackAndWhiteLightColorScheme = ColorScheme<Color?>(
|
||||
primary = StaticColor(0xFF000000.toInt()),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
|
||||
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
|
||||
internal class LegacyColorRefSerializer: KSerializer<ColorRef> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("$", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): ColorRef {
|
||||
return Color.fromString(decoder.decodeString()) as ColorRef
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ColorRef) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
|
||||
internal class LegacyStaticColorSerializer: KSerializer<StaticColor> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("#", PrimitiveKind.STRING)
|
||||
override fun deserialize(decoder: Decoder): StaticColor {
|
||||
return Color.fromString(decoder.decodeString()) as StaticColor
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: StaticColor) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
|
||||
internal val legacyModule = SerializersModule {
|
||||
polymorphic(Color::class) {
|
||||
subclass(ColorRef::class, LegacyColorRefSerializer())
|
||||
subclass(StaticColor::class, LegacyStaticColorSerializer())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
|
||||
val LegacyThemeJson = Json {
|
||||
serializersModule = legacyModule
|
||||
useArrayPolymorphism = true
|
||||
}
|
||||
|
||||
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
|
||||
fun Colors.toLegacyJson(): String {
|
||||
return LegacyThemeJson.encodeToString(this)
|
||||
}
|
||||
|
||||
|
||||
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
|
||||
fun Colors.Companion.fromLegacyJson(json: String): Colors {
|
||||
return try {
|
||||
LegacyThemeJson.decodeFromString(json)
|
||||
} catch (e: SerializationException) {
|
||||
throw IllegalArgumentException(e)
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,39 @@
|
||||
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
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.contextual
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
|
||||
internal class ColorRefSerializer: KSerializer<ColorRef> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("$", PrimitiveKind.STRING)
|
||||
internal class ColorSerializer: KSerializer<Color> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): ColorRef {
|
||||
return Color(decoder.decodeString()) as ColorRef
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ColorRef) {
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: Color
|
||||
) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal class StaticColorSerializer: KSerializer<StaticColor> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("#", PrimitiveKind.STRING)
|
||||
override fun deserialize(decoder: Decoder): StaticColor {
|
||||
return Color(decoder.decodeString()) as StaticColor
|
||||
override fun deserialize(decoder: Decoder): Color {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: StaticColor) {
|
||||
}
|
||||
|
||||
internal class ShapeSerializer: KSerializer<Shape> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ShapeSerializer", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: Shape
|
||||
) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal val module = SerializersModule {
|
||||
polymorphic(Color::class) {
|
||||
subclass(ColorRef::class, ColorRefSerializer())
|
||||
subclass(StaticColor::class, StaticColorSerializer())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val ThemeJson = Json {
|
||||
serializersModule = module
|
||||
useArrayPolymorphism = true
|
||||
}
|
||||
|
||||
fun Theme.toJson(): String {
|
||||
return ThemeJson.encodeToString(this)
|
||||
}
|
||||
|
||||
fun Theme.Companion.fromJson(json: String): Theme {
|
||||
return try {
|
||||
ThemeJson.decodeFromString(json)
|
||||
} catch (e: SerializationException) {
|
||||
throw IllegalArgumentException(e)
|
||||
override fun deserialize(decoder: Decoder): Shape {
|
||||
return Shape.fromString(decoder.decodeString())!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package de.mm20.launcher2.themes
|
||||
|
||||
import de.mm20.launcher2.database.entities.ShapesEntity
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class Shapes(
|
||||
@Transient val id: UUID = UUID.randomUUID(),
|
||||
val builtIn: Boolean = false,
|
||||
val name: String,
|
||||
val baseShape: Shape = Shape(
|
||||
corners = CornerStyle.Rounded,
|
||||
radii = intArrayOf(12, 12, 12, 12),
|
||||
),
|
||||
val extraSmall: Shape? = null,
|
||||
val small: Shape? = null,
|
||||
val medium: Shape? = null,
|
||||
val large: Shape? = null,
|
||||
val largeIncreased: Shape? = null,
|
||||
val extraLarge: Shape? = null,
|
||||
val extraLargeIncreased: Shape? = null,
|
||||
val extraExtraLarge: Shape? = null,
|
||||
) {
|
||||
constructor(entity: ShapesEntity) : this(
|
||||
id = entity.id,
|
||||
builtIn = false,
|
||||
name = entity.name,
|
||||
baseShape = Shape.fromString(entity.baseShape) ?: Shape(
|
||||
corners = CornerStyle.Rounded,
|
||||
radii = intArrayOf(12, 12, 12, 12)
|
||||
),
|
||||
extraSmall = Shape.fromString(entity.extraSmall),
|
||||
small = Shape.fromString(entity.small),
|
||||
medium = Shape.fromString(entity.medium),
|
||||
large = Shape.fromString(entity.large),
|
||||
largeIncreased = Shape.fromString(entity.largeIncreased),
|
||||
extraLarge = Shape.fromString(entity.extraLarge),
|
||||
extraLargeIncreased = Shape.fromString(entity.extraLargeIncreased),
|
||||
extraExtraLarge = Shape.fromString(entity.extraExtraLarge),
|
||||
)
|
||||
|
||||
internal fun toEntity(): ShapesEntity {
|
||||
return ShapesEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
baseShape = baseShape.toString(),
|
||||
extraSmall = extraSmall?.toString(),
|
||||
small = small?.toString(),
|
||||
medium = medium?.toString(),
|
||||
large = large?.toString(),
|
||||
largeIncreased = largeIncreased?.toString(),
|
||||
extraLarge = extraLarge?.toString(),
|
||||
extraLargeIncreased = extraLargeIncreased?.toString(),
|
||||
extraExtraLarge = extraExtraLarge?.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = ShapeSerializer::class)
|
||||
data class Shape(
|
||||
/**
|
||||
* The style of the corners.
|
||||
* null to inherit the corner style from the base shape.
|
||||
*/
|
||||
val corners: CornerStyle? = null,
|
||||
/**
|
||||
* Radii in dp, in the order of top-start, top-end, bottom-end, bottom-start.
|
||||
* null to inherit the radius from the base shape.
|
||||
*/
|
||||
val radii: IntArray? = null,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Shape) return false
|
||||
return corners == other.corners &&
|
||||
radii.contentEquals(other.radii)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = corners.hashCode()
|
||||
result = 31 * result + radii.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val type = when (corners) {
|
||||
CornerStyle.Rounded -> "r"
|
||||
CornerStyle.Cut -> "c"
|
||||
null -> "$"
|
||||
}
|
||||
val radii = radii?.joinToString("|") ?: ""
|
||||
return "$type.${radii}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String?): Shape? {
|
||||
|
||||
if (string == null) return null
|
||||
val parts = string.split('.')
|
||||
val corners = when (parts[0]) {
|
||||
"r" -> CornerStyle.Rounded
|
||||
"c" -> CornerStyle.Cut
|
||||
else -> null
|
||||
}
|
||||
val radii = if (parts.size > 1 && parts[1].isNotEmpty()) {
|
||||
parts[1].split("|").map { it.toInt() }.toIntArray()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return Shape(corners, radii)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CornerStyle {
|
||||
Rounded,
|
||||
Cut,
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import android.content.Context
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.preferences.ThemeDescriptor
|
||||
import de.mm20.launcher2.preferences.ColorsDescriptor
|
||||
import de.mm20.launcher2.preferences.ShapesDescriptor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -16,7 +17,6 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
@@ -26,50 +26,50 @@ class ThemeRepository(
|
||||
) : Backupable {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
|
||||
fun getThemes(): Flow<List<Theme>> {
|
||||
return database.themeDao().getAll().map {
|
||||
getBuiltInThemes() + it.map { Theme(it) }
|
||||
fun getAllColors(): Flow<List<Colors>> {
|
||||
return database.themeDao().getAllColors().map {
|
||||
getBuiltInColors() + it.map { Colors(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getTheme(id: UUID): Flow<Theme?> {
|
||||
if (id == DefaultThemeId) return flowOf(getDefaultTheme())
|
||||
if (id == BlackAndWhiteThemeId) return flowOf(getBlackAndWhiteTheme())
|
||||
return database.themeDao().get(id).map { it?.let { Theme(it) } }.flowOn(Dispatchers.Default)
|
||||
fun getColors(id: UUID): Flow<Colors?> {
|
||||
if (id == DefaultThemeId) return flowOf(getDefaultColors())
|
||||
if (id == BlackAndWhiteThemeId) return flowOf(getBlackAndWhiteColors())
|
||||
return database.themeDao().getColors(id).map { it?.let { Colors(it) } }.flowOn(Dispatchers.Default)
|
||||
}
|
||||
|
||||
fun createTheme(theme: Theme) {
|
||||
fun createColors(colors: Colors) {
|
||||
scope.launch {
|
||||
database.themeDao().insert(theme.toEntity())
|
||||
database.themeDao().insertColors(colors.toEntity())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTheme(theme: Theme) {
|
||||
fun updateColors(colors: Colors) {
|
||||
scope.launch {
|
||||
database.themeDao().update(theme.toEntity())
|
||||
database.themeDao().updateColors(colors.toEntity())
|
||||
}
|
||||
}
|
||||
|
||||
fun getThemeOrDefault(theme: ThemeDescriptor?): Flow<Theme> {
|
||||
fun getColorsOrDefault(theme: ColorsDescriptor?): Flow<Colors> {
|
||||
return when(theme) {
|
||||
is ThemeDescriptor.BlackAndWhite -> flowOf(getBlackAndWhiteTheme())
|
||||
is ThemeDescriptor.Custom -> {
|
||||
is ColorsDescriptor.BlackAndWhite -> flowOf(getBlackAndWhiteColors())
|
||||
is ColorsDescriptor.Custom -> {
|
||||
val id = UUID.fromString(theme.id)
|
||||
getTheme(id).map { it ?: getDefaultTheme() }
|
||||
getColors(id).map { it ?: getDefaultColors() }
|
||||
}
|
||||
else -> flowOf(getDefaultTheme())
|
||||
else -> flowOf(getDefaultColors())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBuiltInThemes(): List<Theme> {
|
||||
private fun getBuiltInColors(): List<Colors> {
|
||||
return listOf(
|
||||
getDefaultTheme(),
|
||||
getBlackAndWhiteTheme(),
|
||||
getDefaultColors(),
|
||||
getBlackAndWhiteColors(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getDefaultTheme(): Theme {
|
||||
return Theme(
|
||||
private fun getDefaultColors(): Colors {
|
||||
return Colors(
|
||||
id = DefaultThemeId,
|
||||
builtIn = true,
|
||||
name = context.getString(R.string.preference_colors_default),
|
||||
@@ -79,8 +79,8 @@ class ThemeRepository(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBlackAndWhiteTheme(): Theme {
|
||||
return Theme(
|
||||
private fun getBlackAndWhiteColors(): Colors {
|
||||
return Colors(
|
||||
id = BlackAndWhiteThemeId,
|
||||
builtIn = true,
|
||||
name = context.getString(R.string.preference_colors_bw),
|
||||
@@ -90,16 +90,127 @@ class ThemeRepository(
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteTheme(theme: Theme) {
|
||||
fun deleteColors(colors: Colors) {
|
||||
scope.launch {
|
||||
database.themeDao().delete(theme.id)
|
||||
database.themeDao().deleteColors(colors.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllShapes(): Flow<List<Shapes>> {
|
||||
return database.themeDao().getAllShapes().map {
|
||||
getBuiltInShapes() + it.map { Shapes(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getShapes(id: UUID): Flow<Shapes?> {
|
||||
if (id == DefaultThemeId) return flowOf(getDefaultShapes())
|
||||
if (id == ExtraRoundShapesId) return flowOf(getExtraRoundShapes())
|
||||
if (id == RectShapesId) return flowOf(getRectShapes())
|
||||
if (id == CutShapesId) return flowOf(getCutShapes())
|
||||
return database.themeDao().getShapes(id).map { it?.let { Shapes(it) } }.flowOn(Dispatchers.Default)
|
||||
}
|
||||
|
||||
fun createShapes(shapes: Shapes) {
|
||||
scope.launch {
|
||||
database.themeDao().insertShapes(shapes.toEntity())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateShapes(shapes: Shapes) {
|
||||
scope.launch {
|
||||
database.themeDao().updateShapes(shapes.toEntity())
|
||||
}
|
||||
}
|
||||
|
||||
fun getShapesOrDefault(theme: ShapesDescriptor?): Flow<Shapes> {
|
||||
return when(theme) {
|
||||
is ShapesDescriptor.Custom -> {
|
||||
val id = UUID.fromString(theme.id)
|
||||
getShapes(id).map { it ?: getDefaultShapes() }
|
||||
}
|
||||
is ShapesDescriptor.ExtraRound -> flowOf(getExtraRoundShapes())
|
||||
is ShapesDescriptor.Rect -> flowOf(getRectShapes())
|
||||
is ShapesDescriptor.Cut -> flowOf(getCutShapes())
|
||||
else -> flowOf(getDefaultShapes())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBuiltInShapes(): List<Shapes> {
|
||||
return listOf(
|
||||
getDefaultShapes(),
|
||||
getExtraRoundShapes(),
|
||||
getRectShapes(),
|
||||
getCutShapes(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDefaultShapes(): Shapes {
|
||||
return Shapes(
|
||||
id = DefaultThemeId,
|
||||
builtIn = true,
|
||||
name = context.getString(R.string.preference_shapes_default),
|
||||
baseShape = Shape(
|
||||
corners = CornerStyle.Rounded,
|
||||
radii = intArrayOf(12, 12, 12, 12),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCutShapes(): Shapes {
|
||||
return Shapes(
|
||||
id = CutShapesId,
|
||||
builtIn = true,
|
||||
name = context.getString(R.string.preference_cards_shape_cut),
|
||||
baseShape = Shape(
|
||||
corners = CornerStyle.Cut,
|
||||
radii = intArrayOf(12, 12, 12, 12),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExtraRoundShapes(): Shapes {
|
||||
return Shapes(
|
||||
id = ExtraRoundShapesId,
|
||||
builtIn = true,
|
||||
name = context.getString(R.string.preference_shapes_extra_round),
|
||||
baseShape = Shape(
|
||||
corners = CornerStyle.Rounded,
|
||||
radii = intArrayOf(24, 24, 24, 24),
|
||||
),
|
||||
extraLarge = Shape(
|
||||
radii = intArrayOf(36, 36, 36, 36),
|
||||
),
|
||||
extraLargeIncreased = Shape(
|
||||
radii = intArrayOf(40, 40, 40, 40),
|
||||
),
|
||||
extraExtraLarge = Shape(
|
||||
radii = intArrayOf(56, 56, 56, 56),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRectShapes(): Shapes {
|
||||
return Shapes(
|
||||
id = RectShapesId,
|
||||
builtIn = true,
|
||||
name = context.getString(R.string.preference_shapes_rect),
|
||||
baseShape = Shape(
|
||||
corners = CornerStyle.Rounded,
|
||||
radii = intArrayOf(0, 0, 0, 0),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteShapes(shapes: Shapes) {
|
||||
scope.launch {
|
||||
database.themeDao().deleteShapes(shapes.id)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
|
||||
val dao = database.themeDao()
|
||||
val themes = dao.getAll().first().map { Theme(it) }
|
||||
val data = ThemeJson.encodeToString(themes)
|
||||
val colors = dao.getAllColors().first().map { Colors(it) }
|
||||
val data = LegacyThemeJson.encodeToString(colors)
|
||||
|
||||
val file = File(toDir, "themes.0000")
|
||||
file.bufferedWriter().use {
|
||||
@@ -109,7 +220,7 @@ class ThemeRepository(
|
||||
|
||||
override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
|
||||
val dao = database.themeDao()
|
||||
dao.deleteAll()
|
||||
dao.deleteAllColors()
|
||||
|
||||
val files =
|
||||
fromDir.listFiles { _, name -> name.startsWith("themes.") }
|
||||
@@ -117,8 +228,8 @@ class ThemeRepository(
|
||||
|
||||
for (file in files) {
|
||||
val data = file.inputStream().reader().readText()
|
||||
val themes: List<Theme> = try {
|
||||
ThemeJson.decodeFromString(data)
|
||||
val colors: List<Colors> = try {
|
||||
LegacyThemeJson.decodeFromString(data)
|
||||
} catch (e: SerializationException) {
|
||||
CrashReporter.logException(e)
|
||||
continue
|
||||
@@ -126,7 +237,7 @@ class ThemeRepository(
|
||||
CrashReporter.logException(e)
|
||||
continue
|
||||
}
|
||||
dao.insertAll(themes.map { it.toEntity() })
|
||||
dao.insertAllColors(colors.map { it.toEntity() })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user