diff --git a/README.md b/README.md index dd64a50..30f71db 100644 --- a/README.md +++ b/README.md @@ -149,15 +149,54 @@ Snitcher.install( ### Custom Snitcher Theme -If you just want to use the pre-builts sreens, but want to customize those components, such as colors and strings, you can easily accomplish it by giving a copy of `SnitcherColor` to the `SnitcherTheme`: +The pre-built screens are styled by a `SnitcherThemeConfig`. Give one to `Snitcher.install`, and the built-in `ExceptionTraceActivity`, as well as every screen you wrap in `SnitcherTheme`, will be drawn with it: + +```kotlin +Snitcher.install( + application = this, + theme = SnitcherThemeConfig( + lightColors = SnitcherColor.defaultColors().copy(primary = Color(0xFF6650a4)), + darkColors = SnitcherColor.defaultDarkColors().copy(primary = Color(0xFFD0BCFF)), + typography = SnitcherTypography.defaultTypography().copy( + title = TextStyle(fontSize = 30.sp, fontWeight = FontWeight.Black), + ), + shapes = SnitcherShapes(button = RoundedCornerShape(20.dp)), + ), +) +``` + +`lightColors` and `darkColors` are picked by the system dark mode. Every field has a default, so you only need to declare what you want to change. + +**SnitcherColor** + +| Property | Where it is used | +|---|---| +| `primary` | The title, the section labels, and the button background | +| `onPrimary` | The content that is drawn on the buttons | +| `background` | The screen background, which is also drawn behind the system bars | +| `textHighEmphasis` | The exception message, the stack trace, and the restore screen texts | +| `textLowEmphasis` | The package and device information | +| `outline` | The border of the stack trace container | + +**SnitcherTypography** + +`title`, `message`, `deviceInfo`, `sectionLabel`, `stacktrace`, and `button` are plain `TextStyle`s, so fonts, sizes, and weights are all yours. + +**SnitcherShapes** + +`button` and `stacktrace` are plain `Shape`s. + +You can also swap the theme at any time, which restyles the screens right away: + +```kotlin +Snitcher.theme = Snitcher.theme.copy(darkColors = SnitcherColor.defaultDarkColors()) +``` + +If you build your own trace screens, wrap them in `SnitcherTheme`. Its defaults come from the installed configuration, so your screens and the pre-built ones stay in sync, and you can still override any of them for a single screen: ```kotlin SnitcherTheme( - colors = SnitcherTheme.colors.copy( - primary = Color.Blue, - background = Color.White, - textHighEmphasis = Color.Black - ) + colors = SnitcherTheme.colors.copy(primary = Color.Blue), ) { if (exception != null) { ExceptionTraceScreen( diff --git a/demo/src/main/kotlin/com/skydoves/snitcherdemo/SnitcherDemoApp.kt b/demo/src/main/kotlin/com/skydoves/snitcherdemo/SnitcherDemoApp.kt index ec62449..055226c 100644 --- a/demo/src/main/kotlin/com/skydoves/snitcherdemo/SnitcherDemoApp.kt +++ b/demo/src/main/kotlin/com/skydoves/snitcherdemo/SnitcherDemoApp.kt @@ -16,7 +16,18 @@ package com.skydoves.snitcherdemo import android.app.Application +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.skydoves.snitcher.Snitcher +import com.skydoves.snitcher.ui.theme.SnitcherColor +import com.skydoves.snitcher.ui.theme.SnitcherShapes +import com.skydoves.snitcher.ui.theme.SnitcherThemeConfig +import com.skydoves.snitcher.ui.theme.SnitcherTypography +import com.skydoves.snitcherdemo.ui.theme.Purple40 +import com.skydoves.snitcherdemo.ui.theme.Purple80 class SnitcherDemoApp : Application() { @@ -25,6 +36,14 @@ class SnitcherDemoApp : Application() { Snitcher.install( application = this, + theme = SnitcherThemeConfig( + lightColors = SnitcherColor.defaultColors().copy(primary = Purple40), + darkColors = SnitcherColor.defaultDarkColors().copy(primary = Purple80), + typography = SnitcherTypography.defaultTypography().copy( + title = TextStyle(fontSize = 30.sp, fontWeight = FontWeight.Black), + ), + shapes = SnitcherShapes(button = RoundedCornerShape(20.dp)), + ), exceptionHandler = { // do something }, diff --git a/docs/custom-theme.md b/docs/custom-theme.md index f5b7708..bc7a754 100644 --- a/docs/custom-theme.md +++ b/docs/custom-theme.md @@ -1,14 +1,53 @@ # Snitcher Theme -If you just want to use the pre-builts sreens, but want to customize those components, such as colors and strings, you can easily accomplish it by giving a copy of `SnitcherColor` to the `SnitcherTheme`: +The pre-built screens are styled by a `SnitcherThemeConfig`. Give one to `Snitcher.install`, and the built-in `ExceptionTraceActivity`, as well as every screen you wrap in `SnitcherTheme`, will be drawn with it: + +```kotlin +Snitcher.install( + application = this, + theme = SnitcherThemeConfig( + lightColors = SnitcherColor.defaultColors().copy(primary = Color(0xFF6650a4)), + darkColors = SnitcherColor.defaultDarkColors().copy(primary = Color(0xFFD0BCFF)), + typography = SnitcherTypography.defaultTypography().copy( + title = TextStyle(fontSize = 30.sp, fontWeight = FontWeight.Black), + ), + shapes = SnitcherShapes(button = RoundedCornerShape(20.dp)), + ), +) +``` + +`lightColors` and `darkColors` are picked by the system dark mode. Every field has a default, so you only need to declare what you want to change. + +**SnitcherColor** + +| Property | Where it is used | +|---|---| +| `primary` | The title, the section labels, and the button background | +| `onPrimary` | The content that is drawn on the buttons | +| `background` | The screen background, which is also drawn behind the system bars | +| `textHighEmphasis` | The exception message, the stack trace, and the restore screen texts | +| `textLowEmphasis` | The package and device information | +| `outline` | The border of the stack trace container | + +**SnitcherTypography** + +`title`, `message`, `deviceInfo`, `sectionLabel`, `stacktrace`, and `button` are plain `TextStyle`s, so fonts, sizes, and weights are all yours. + +**SnitcherShapes** + +`button` and `stacktrace` are plain `Shape`s. + +You can also swap the theme at any time, which restyles the screens right away: + +```kotlin +Snitcher.theme = Snitcher.theme.copy(darkColors = SnitcherColor.defaultDarkColors()) +``` + +If you build your own trace screens, wrap them in `SnitcherTheme`. Its defaults come from the installed configuration, so your screens and the pre-built ones stay in sync, and you can still override any of them for a single screen: ```kotlin SnitcherTheme( - colors = SnitcherTheme.colors.copy( - primary = Color.Blue, - background = Color.White, - textHighEmphasis = Color.Black - ) + colors = SnitcherTheme.colors.copy(primary = Color.Blue), ) { if (exception != null) { ExceptionTraceScreen( diff --git a/snitcher/api/snitcher.api b/snitcher/api/snitcher.api index 136a823..f350aee 100644 --- a/snitcher/api/snitcher.api +++ b/snitcher/api/snitcher.api @@ -8,12 +8,14 @@ public final class com/skydoves/snitcher/Snitcher : java/lang/Thread$UncaughtExc public final class com/skydoves/snitcher/Snitcher$Companion { public final fun getException ()Lkotlinx/coroutines/flow/StateFlow; public final fun getLauncher ()Lkotlinx/coroutines/flow/StateFlow; - public final fun install (Landroid/app/Application;Lkotlin/reflect/KClass;Lcom/skydoves/snitcher/TraceStrategy;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun install$default (Lcom/skydoves/snitcher/Snitcher$Companion;Landroid/app/Application;Lkotlin/reflect/KClass;Lcom/skydoves/snitcher/TraceStrategy;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun getTheme ()Lcom/skydoves/snitcher/ui/theme/SnitcherThemeConfig; + public final fun install (Landroid/app/Application;Lkotlin/reflect/KClass;Lcom/skydoves/snitcher/TraceStrategy;Lcom/skydoves/snitcher/ui/theme/SnitcherThemeConfig;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun install$default (Lcom/skydoves/snitcher/Snitcher$Companion;Landroid/app/Application;Lkotlin/reflect/KClass;Lcom/skydoves/snitcher/TraceStrategy;Lcom/skydoves/snitcher/ui/theme/SnitcherThemeConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun isDebuggable ()Z public final fun isDebuggableApp (Landroid/app/Application;)Z public final fun isInstalled ()Z public final fun setDebuggable (Z)V + public final fun setTheme (Lcom/skydoves/snitcher/ui/theme/SnitcherThemeConfig;)V } public final class com/skydoves/snitcher/TraceStrategy : java/lang/Enum { @@ -131,32 +133,104 @@ public final class com/skydoves/snitcher/ui/ExceptionTraceScreenKt { public final class com/skydoves/snitcher/ui/theme/SnitcherColor { public static final field $stable I public static final field Companion Lcom/skydoves/snitcher/ui/theme/SnitcherColor$Companion; - public synthetic fun (JJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component2-0d7_KjU ()J public final fun component3-0d7_KjU ()J - public final fun copy-ysEtTa8 (JJJ)Lcom/skydoves/snitcher/ui/theme/SnitcherColor; - public static synthetic fun copy-ysEtTa8$default (Lcom/skydoves/snitcher/ui/theme/SnitcherColor;JJJILjava/lang/Object;)Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun component4-0d7_KjU ()J + public final fun component5-0d7_KjU ()J + public final fun component6-0d7_KjU ()J + public final fun copy-tNS2XkQ (JJJJJJ)Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public static synthetic fun copy-tNS2XkQ$default (Lcom/skydoves/snitcher/ui/theme/SnitcherColor;JJJJJJILjava/lang/Object;)Lcom/skydoves/snitcher/ui/theme/SnitcherColor; public fun equals (Ljava/lang/Object;)Z public final fun getBackground-0d7_KjU ()J + public final fun getOnPrimary-0d7_KjU ()J + public final fun getOutline-0d7_KjU ()J public final fun getPrimary-0d7_KjU ()J public final fun getTextHighEmphasis-0d7_KjU ()J + public final fun getTextLowEmphasis-0d7_KjU ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class com/skydoves/snitcher/ui/theme/SnitcherColor$Companion { - public final fun defaultColors (Landroidx/compose/runtime/Composer;I)Lcom/skydoves/snitcher/ui/theme/SnitcherColor; - public final fun defaultDarkColors (Landroidx/compose/runtime/Composer;I)Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun defaultColors ()Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun defaultDarkColors ()Lcom/skydoves/snitcher/ui/theme/SnitcherColor; +} + +public final class com/skydoves/snitcher/ui/theme/SnitcherShapes { + public static final field $stable I + public fun ()V + public fun (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V + public synthetic fun (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Landroidx/compose/ui/graphics/Shape; + public final fun component2 ()Landroidx/compose/ui/graphics/Shape; + public final fun copy (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)Lcom/skydoves/snitcher/ui/theme/SnitcherShapes; + public static synthetic fun copy$default (Lcom/skydoves/snitcher/ui/theme/SnitcherShapes;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;ILjava/lang/Object;)Lcom/skydoves/snitcher/ui/theme/SnitcherShapes; + public fun equals (Ljava/lang/Object;)Z + public final fun getButton ()Landroidx/compose/ui/graphics/Shape; + public final fun getStacktrace ()Landroidx/compose/ui/graphics/Shape; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/skydoves/snitcher/ui/theme/SnitcherTheme { public static final field $stable I public static final field INSTANCE Lcom/skydoves/snitcher/ui/theme/SnitcherTheme; public final fun getColors (Landroidx/compose/runtime/Composer;I)Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun getShapes (Landroidx/compose/runtime/Composer;I)Lcom/skydoves/snitcher/ui/theme/SnitcherShapes; + public final fun getTypography (Landroidx/compose/runtime/Composer;I)Lcom/skydoves/snitcher/ui/theme/SnitcherTypography; +} + +public final class com/skydoves/snitcher/ui/theme/SnitcherThemeConfig { + public static final field $stable I + public fun ()V + public fun (Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherTypography;Lcom/skydoves/snitcher/ui/theme/SnitcherShapes;)V + public synthetic fun (Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherTypography;Lcom/skydoves/snitcher/ui/theme/SnitcherShapes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun component2 ()Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun component3 ()Lcom/skydoves/snitcher/ui/theme/SnitcherTypography; + public final fun component4 ()Lcom/skydoves/snitcher/ui/theme/SnitcherShapes; + public final fun copy (Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherTypography;Lcom/skydoves/snitcher/ui/theme/SnitcherShapes;)Lcom/skydoves/snitcher/ui/theme/SnitcherThemeConfig; + public static synthetic fun copy$default (Lcom/skydoves/snitcher/ui/theme/SnitcherThemeConfig;Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherTypography;Lcom/skydoves/snitcher/ui/theme/SnitcherShapes;ILjava/lang/Object;)Lcom/skydoves/snitcher/ui/theme/SnitcherThemeConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getDarkColors ()Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun getLightColors ()Lcom/skydoves/snitcher/ui/theme/SnitcherColor; + public final fun getShapes ()Lcom/skydoves/snitcher/ui/theme/SnitcherShapes; + public final fun getTypography ()Lcom/skydoves/snitcher/ui/theme/SnitcherTypography; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/skydoves/snitcher/ui/theme/SnitcherThemeKt { - public static final fun SnitcherTheme (ZLcom/skydoves/snitcher/ui/theme/SnitcherColor;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun SnitcherTheme (ZLcom/skydoves/snitcher/ui/theme/SnitcherColor;Lcom/skydoves/snitcher/ui/theme/SnitcherTypography;Lcom/skydoves/snitcher/ui/theme/SnitcherShapes;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/skydoves/snitcher/ui/theme/SnitcherTypography { + public static final field $stable I + public static final field Companion Lcom/skydoves/snitcher/ui/theme/SnitcherTypography$Companion; + public fun (Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;)V + public final fun component1 ()Landroidx/compose/ui/text/TextStyle; + public final fun component2 ()Landroidx/compose/ui/text/TextStyle; + public final fun component3 ()Landroidx/compose/ui/text/TextStyle; + public final fun component4 ()Landroidx/compose/ui/text/TextStyle; + public final fun component5 ()Landroidx/compose/ui/text/TextStyle; + public final fun component6 ()Landroidx/compose/ui/text/TextStyle; + public final fun copy (Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;)Lcom/skydoves/snitcher/ui/theme/SnitcherTypography; + public static synthetic fun copy$default (Lcom/skydoves/snitcher/ui/theme/SnitcherTypography;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;ILjava/lang/Object;)Lcom/skydoves/snitcher/ui/theme/SnitcherTypography; + public fun equals (Ljava/lang/Object;)Z + public final fun getButton ()Landroidx/compose/ui/text/TextStyle; + public final fun getDeviceInfo ()Landroidx/compose/ui/text/TextStyle; + public final fun getMessage ()Landroidx/compose/ui/text/TextStyle; + public final fun getSectionLabel ()Landroidx/compose/ui/text/TextStyle; + public final fun getStacktrace ()Landroidx/compose/ui/text/TextStyle; + public final fun getTitle ()Landroidx/compose/ui/text/TextStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/snitcher/ui/theme/SnitcherTypography$Companion { + public final fun defaultTypography ()Lcom/skydoves/snitcher/ui/theme/SnitcherTypography; } diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/Snitcher.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/Snitcher.kt index d4e1e12..26dcc71 100644 --- a/snitcher/src/main/kotlin/com/skydoves/snitcher/Snitcher.kt +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/Snitcher.kt @@ -21,6 +21,9 @@ import android.content.Intent import android.content.pm.ApplicationInfo import android.os.Bundle import android.os.Process +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.skydoves.snitcher.Snitcher.Companion.install @@ -31,6 +34,7 @@ import com.skydoves.snitcher.model.SnitcherException import com.skydoves.snitcher.model.SnitcherPreference import com.skydoves.snitcher.model.toSnitcherElement import com.skydoves.snitcher.ui.ExceptionTraceActivity +import com.skydoves.snitcher.ui.theme.SnitcherThemeConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -200,6 +204,13 @@ public class Snitcher( */ public val launcher: StateFlow = _launcher + /** + * The theme that styles the pre-built Snitcher screens, such as [ExceptionTraceActivity]. + * It is given by the `theme` parameter of [install], and it can also be replaced at any time + * to restyle the screens on the fly. + */ + public var theme: SnitcherThemeConfig by mutableStateOf(SnitcherThemeConfig()) + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { @@ -234,6 +245,7 @@ public class Snitcher( * @param application Application. * @param traceActivity An Activity that will be launched when your app experiences a crash. * @param traceStrategy The trace strategy determines the launch behaviors when your app experiences a crash. + * @param theme The theme that styles the pre-built exception tracing screens. * @param exceptionHandler You can manage extra exception handlers, like logging exceptions on Firebase, by providing this lambda function here. * This handler will be called with a given [SnitcherException] when your app encounters exceptions. */ @@ -241,9 +253,11 @@ public class Snitcher( application: Application, traceActivity: KClass<*> = ExceptionTraceActivity::class, traceStrategy: TraceStrategy = TraceStrategy.CO_WORK, + theme: SnitcherThemeConfig = SnitcherThemeConfig(), exceptionHandler: (SnitcherException) -> Unit = {}, ) { val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() ?: return + this.theme = theme SnitcherInstaller.install( Snitcher( application = application, @@ -263,6 +277,7 @@ public class Snitcher( * @param traceActivity An Activity that will be launched when your app experiences a crash. * @param traceStrategy The trace strategy determines the launch behaviors when your app experiences a crash. * @param launcher A launcher Activity that used to restore your application from a crash. + * @param theme The theme that styles the pre-built exception tracing screens. * @param exceptionHandler You can manage extra exception handlers, like logging exceptions on Firebase, by providing this lambda function here. * This handler will be called with a given [SnitcherException] when your app encounters exceptions. */ @@ -271,9 +286,11 @@ public class Snitcher( traceActivity: KClass<*> = ExceptionTraceActivity::class, traceStrategy: TraceStrategy = TraceStrategy.CO_WORK, launcher: KClass = T::class, + theme: SnitcherThemeConfig = SnitcherThemeConfig(), noinline exceptionHandler: (SnitcherException) -> Unit = {}, ) { val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() ?: return + this.theme = theme SnitcherInstaller.install( Snitcher( application = application, diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/AppRestoreScreen.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/AppRestoreScreen.kt index e8ae4c3..99386c7 100644 --- a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/AppRestoreScreen.kt +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/AppRestoreScreen.kt @@ -31,9 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.skydoves.snitcher.R import com.skydoves.snitcher.extensions.findActivity import com.skydoves.snitcher.ui.theme.SnitcherStatusBarColor @@ -62,15 +60,14 @@ private fun AppRestoreScreenContent(packageName: String) { Text( text = stringResource(id = R.string.snitcher_release_crash_screen_title), color = SnitcherTheme.colors.textHighEmphasis, - fontWeight = FontWeight.Bold, - fontSize = 34.sp, + style = SnitcherTheme.typography.title, ) Text( modifier = Modifier.padding(vertical = 18.dp), text = stringResource(id = R.string.snitcher_release_crash_screen_description), color = SnitcherTheme.colors.textHighEmphasis, - fontSize = 18.sp, + style = SnitcherTheme.typography.message, ) SnitcherPrimaryButton( diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/ExceptionTraceScreen.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/ExceptionTraceScreen.kt index 5eb0c9d..832a089 100644 --- a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/ExceptionTraceScreen.kt +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/ExceptionTraceScreen.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.Text @@ -50,9 +49,7 @@ import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.skydoves.snitcher.R import com.skydoves.snitcher.extensions.findActivity import com.skydoves.snitcher.extensions.packageInfo @@ -102,23 +99,22 @@ private fun ExceptionTraceScreenContent( Text( text = title, color = SnitcherTheme.colors.primary, - fontWeight = FontWeight.Bold, - fontSize = 34.sp, + style = SnitcherTheme.typography.title, ) Text( modifier = Modifier.padding(vertical = 6.dp), text = snitcherException.message, color = SnitcherTheme.colors.textHighEmphasis, - fontSize = 18.sp, + style = SnitcherTheme.typography.message, ) Text( modifier = Modifier.padding(vertical = 6.dp), text = "${packageInfo?.versionName} (${packageInfo?.versionCode()}) " + "${Build.MANUFACTURER} ${Build.MODEL}", - color = SnitcherTheme.colors.textHighEmphasis, - fontSize = 18.sp, + color = SnitcherTheme.colors.textLowEmphasis, + style = SnitcherTheme.typography.deviceInfo, ) SnitcherPrimaryButton( @@ -149,8 +145,7 @@ private fun ExceptionTraceScreenContent( modifier = Modifier.align(Alignment.CenterStart), text = stacktraceLabel, color = SnitcherTheme.colors.primary, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, + style = SnitcherTheme.typography.sectionLabel, ) Icon( @@ -177,13 +172,13 @@ private fun ExceptionTraceScreenContent( modifier = Modifier .border( - border = BorderStroke(2.dp, SnitcherTheme.colors.primary), - shape = RoundedCornerShape(6.dp), + border = BorderStroke(2.dp, SnitcherTheme.colors.outline), + shape = SnitcherTheme.shapes.stacktrace, ) .padding(12.dp), text = snitcherException.stackTrace, color = SnitcherTheme.colors.textHighEmphasis, - fontSize = 14.sp, + style = SnitcherTheme.typography.stacktrace, ) } } diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/SnitcherPrimaryButton.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/SnitcherPrimaryButton.kt index daec7d8..be48aa8 100644 --- a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/SnitcherPrimaryButton.kt +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/SnitcherPrimaryButton.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon @@ -32,7 +31,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.skydoves.snitcher.ui.theme.SnitcherTheme @Composable @@ -42,6 +40,7 @@ internal fun SnitcherPrimaryButton( text: String = "", onClick: () -> Unit, enabled: Boolean = true, + backgroundColor: Color? = null, contentColor: Color? = null, content: @Composable (RowScope.() -> Unit)? = null, ) { @@ -49,12 +48,12 @@ internal fun SnitcherPrimaryButton( modifier = modifier .fillMaxWidth() .heightIn(min = 54.dp), - shape = RoundedCornerShape(8.dp), + shape = SnitcherTheme.shapes.button, enabled = enabled, colors = ButtonDefaults.buttonColors( - contentColor = contentColor ?: SnitcherTheme.colors.primary, - backgroundColor = contentColor ?: SnitcherTheme.colors.primary, + contentColor = contentColor ?: SnitcherTheme.colors.onPrimary, + backgroundColor = backgroundColor ?: SnitcherTheme.colors.primary, ), onClick = onClick, content = content @@ -65,7 +64,7 @@ internal fun SnitcherPrimaryButton( ) { Icon( imageVector = icon, - tint = Color.White, + tint = contentColor ?: SnitcherTheme.colors.onPrimary, contentDescription = null, ) @@ -73,8 +72,8 @@ internal fun SnitcherPrimaryButton( Text( text = text, - color = Color.White, - fontSize = 16.sp, + color = contentColor ?: SnitcherTheme.colors.onPrimary, + style = SnitcherTheme.typography.button, ) } }, diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherColor.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherColor.kt index 6b5faed..24ac20e 100644 --- a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherColor.kt +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherColor.kt @@ -15,19 +15,31 @@ */ package com.skydoves.snitcher.ui.theme -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse import com.skydoves.snitcher.ui.ExceptionTraceScreen /** * A collection of colors, which are contained by [SnitcherTheme] to implement [ExceptionTraceScreen]. + * + * @property primary The accent color, which is used for the title, the section labels, and the buttons. + * @property onPrimary The content color that is drawn on top of [primary], such as the button labels. + * @property background The background color of the Snitcher screens, which is also drawn behind the system bars. + * @property textHighEmphasis The color of the primary text, such as the exception message and the stack trace. + * @property textLowEmphasis The color of the secondary text, such as the package and device information. + * When it is [Color.Unspecified], a dimmed [textHighEmphasis] is used. + * @property outline The color of the outlines, such as the border of the stack trace container. + * When it is [Color.Unspecified], [primary] is used. */ @Immutable public data class SnitcherColor( val primary: Color, val background: Color, val textHighEmphasis: Color, + val onPrimary: Color = Color.White, + val textLowEmphasis: Color = Color.Unspecified, + val outline: Color = Color.Unspecified, ) { public companion object { /** @@ -35,7 +47,6 @@ public data class SnitcherColor( * * @return A [SnitcherColor] instance holding our color palette. */ - @Composable public fun defaultColors(): SnitcherColor = SnitcherColor( primary = Color(0XFF28a9f1), background = Color.White, @@ -47,7 +58,6 @@ public data class SnitcherColor( * * @return A [SnitcherColor] instance holding our color palette. */ - @Composable public fun defaultDarkColors(): SnitcherColor = SnitcherColor( primary = Color(0XFF28a9f1), background = Color.Black, @@ -55,3 +65,14 @@ public data class SnitcherColor( ) } } + +/** + * Fills the colors that were left unspecified with the ones they follow, so that changing a single + * color, such as `SnitcherColor.defaultColors().copy(primary = Color.Red)`, keeps the palette + * consistent. + */ +@JvmSynthetic +internal fun SnitcherColor.resolveUnspecified(): SnitcherColor = copy( + textLowEmphasis = textLowEmphasis.takeOrElse { textHighEmphasis.copy(alpha = 0.6f) }, + outline = outline.takeOrElse { primary }, +) diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherShapes.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherShapes.kt new file mode 100644 index 0000000..65e09d0 --- /dev/null +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherShapes.kt @@ -0,0 +1,35 @@ +/* + * Designed and developed by 2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.snitcher.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import com.skydoves.snitcher.ui.ExceptionTraceScreen + +/** + * A collection of shapes, which are contained by [SnitcherTheme] to implement + * [ExceptionTraceScreen]. + * + * @property button The shape of the buttons. + * @property stacktrace The shape of the stack trace container. + */ +@Immutable +public data class SnitcherShapes( + val button: Shape = RoundedCornerShape(8.dp), + val stacktrace: Shape = RoundedCornerShape(6.dp), +) diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherTheme.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherTheme.kt index 17abfd6..e077b38 100644 --- a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherTheme.kt +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherTheme.kt @@ -20,6 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import com.skydoves.snitcher.Snitcher import com.skydoves.snitcher.ui.ExceptionTraceScreen /** @@ -29,19 +31,44 @@ private val LocalColors = compositionLocalOf { error("No colors provided! Make sure to wrap all usages of Snitcher components in SnitcherTheme.") } -/** Snitcher Themes to be used for customizing [ExceptionTraceScreen]. */ +private val LocalTypography = compositionLocalOf { + error( + "No typography provided! Make sure to wrap all usages of Snitcher components in SnitcherTheme.", + ) +} + +private val LocalShapes = compositionLocalOf { + error("No shapes provided! Make sure to wrap all usages of Snitcher components in SnitcherTheme.") +} + +/** + * Snitcher Themes to be used for customizing [ExceptionTraceScreen]. + * + * The default values are taken from the [SnitcherThemeConfig] that was given to + * [Snitcher.install], so the pre-built screens and your own screens share the same theme. + * + * @param darkTheme Whether the dark colors should be used. + * @param colors The colors to be used by the Snitcher components. + * @param typography The text styles to be used by the Snitcher components. + * @param shapes The shapes to be used by the Snitcher components. + * @param content The content that will be styled with this theme. + */ @Composable public fun SnitcherTheme( darkTheme: Boolean = isSystemInDarkTheme(), colors: SnitcherColor = if (darkTheme) { - SnitcherColor.defaultDarkColors() + Snitcher.theme.darkColors } else { - SnitcherColor.defaultColors() + Snitcher.theme.lightColors }, + typography: SnitcherTypography = Snitcher.theme.typography, + shapes: SnitcherShapes = Snitcher.theme.shapes, content: @Composable () -> Unit, ) { CompositionLocalProvider( - LocalColors provides colors, + LocalColors provides remember(colors) { colors.resolveUnspecified() }, + LocalTypography provides typography, + LocalShapes provides shapes, ) { content() } @@ -55,4 +82,20 @@ public object SnitcherTheme { @Composable @ReadOnlyComposable get() = LocalColors.current + + /** + * Retrieves the current [SnitcherTypography] at the call site's position in the hierarchy. + */ + public val typography: SnitcherTypography + @Composable + @ReadOnlyComposable + get() = LocalTypography.current + + /** + * Retrieves the current [SnitcherShapes] at the call site's position in the hierarchy. + */ + public val shapes: SnitcherShapes + @Composable + @ReadOnlyComposable + get() = LocalShapes.current } diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherThemeConfig.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherThemeConfig.kt new file mode 100644 index 0000000..b30a614 --- /dev/null +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherThemeConfig.kt @@ -0,0 +1,48 @@ +/* + * Designed and developed by 2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.snitcher.ui.theme + +import androidx.compose.runtime.Immutable +import com.skydoves.snitcher.Snitcher +import com.skydoves.snitcher.ui.ExceptionTraceActivity + +/** + * A theme configuration for the pre-built Snitcher screens. Give an instance of this class to the + * `theme` parameter of [Snitcher.install], and the built-in [ExceptionTraceActivity] and every + * screen wrapped in [SnitcherTheme] will be styled with it. + * + * ```kotlin + * Snitcher.install( + * application = this, + * theme = SnitcherThemeConfig( + * lightColors = SnitcherColor.defaultColors().copy(primary = Color(0xFFEF5350)), + * shapes = SnitcherShapes(button = CircleShape), + * ), + * ) + * ``` + * + * @property lightColors The colors that are used when the system is in light mode. + * @property darkColors The colors that are used when the system is in dark mode. + * @property typography The text styles of the Snitcher screens. + * @property shapes The shapes of the Snitcher screens. + */ +@Immutable +public data class SnitcherThemeConfig( + val lightColors: SnitcherColor = SnitcherColor.defaultColors(), + val darkColors: SnitcherColor = SnitcherColor.defaultDarkColors(), + val typography: SnitcherTypography = SnitcherTypography.defaultTypography(), + val shapes: SnitcherShapes = SnitcherShapes(), +) diff --git a/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherTypography.kt b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherTypography.kt new file mode 100644 index 0000000..68691e7 --- /dev/null +++ b/snitcher/src/main/kotlin/com/skydoves/snitcher/ui/theme/SnitcherTypography.kt @@ -0,0 +1,59 @@ +/* + * Designed and developed by 2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.snitcher.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.skydoves.snitcher.ui.ExceptionTraceScreen + +/** + * A collection of text styles, which are contained by [SnitcherTheme] to implement + * [ExceptionTraceScreen]. + * + * @property title The style of the screen titles, such as the exception name. + * @property message The style of the exception message and the restore screen description. + * @property deviceInfo The style of the package and device information. + * @property sectionLabel The style of the section labels, such as the stack trace label. + * @property stacktrace The style of the stack trace body. + * @property button The style of the button labels. + */ +@Immutable +public data class SnitcherTypography( + val title: TextStyle, + val message: TextStyle, + val deviceInfo: TextStyle, + val sectionLabel: TextStyle, + val stacktrace: TextStyle, + val button: TextStyle, +) { + public companion object { + /** + * Provides the default text styles of the Snitcher screens. + * + * @return A [SnitcherTypography] instance holding our text styles. + */ + public fun defaultTypography(): SnitcherTypography = SnitcherTypography( + title = TextStyle(fontSize = 34.sp, fontWeight = FontWeight.Bold), + message = TextStyle(fontSize = 18.sp), + deviceInfo = TextStyle(fontSize = 18.sp), + sectionLabel = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold), + stacktrace = TextStyle(fontSize = 14.sp), + button = TextStyle(fontSize = 16.sp), + ) + } +}