Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-24 18:55:42 +01:00
parent a629de6271
commit 3cef7c5092
2161 changed files with 246605 additions and 2 deletions

View file

@ -0,0 +1,3 @@
## Core - UI - Compose - Common
This module contains common code for the compose UI.

View file

@ -0,0 +1,8 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "app.k9mail.core.ui.compose.common"
resourcePrefix = "core_ui_common_"
}

View file

@ -0,0 +1,16 @@
package app.k9mail.core.ui.compose.common
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
/**
* A marker annotation for device previews.
*
* It's used to provide previews for a set of different devices and form factors.
*/
@Preview(name = "Phone", device = Devices.PHONE)
@Preview(name = "Phone landscape", device = "spec:shape=Normal,width=891,height=411,unit=dp,dpi=420")
@Preview(name = "Foldable", device = Devices.FOLDABLE)
@Preview(name = "Tablet", device = Devices.TABLET)
@Preview(name = "Desktop", device = Devices.DESKTOP)
annotation class DevicePreviews

View file

@ -0,0 +1,37 @@
package app.k9mail.core.ui.compose.common.window
/**
* WindowSizeClass as defined by supporting different screen sizes.
*
* See: https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes#window_size_classes
*/
enum class WindowSizeClass {
Compact,
Medium,
Expanded,
;
companion object {
const val COMPACT_MAX_WIDTH = 600
const val COMPACT_MAX_HEIGHT = 480
const val MEDIUM_MAX_WIDTH = 840
const val MEDIUM_MAX_HEIGHT = 900
fun fromWidth(width: Int): WindowSizeClass {
return when {
width < COMPACT_MAX_WIDTH -> Compact
width < MEDIUM_MAX_WIDTH -> Medium
else -> Expanded
}
}
fun fromHeight(height: Int): WindowSizeClass {
return when {
height < COMPACT_MAX_HEIGHT -> Compact
height < MEDIUM_MAX_HEIGHT -> Medium
else -> Expanded
}
}
}
}

View file

@ -0,0 +1,30 @@
package app.k9mail.core.ui.compose.common.window
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Returns the current window size info based on current Configuration.
*/
@Composable
fun getWindowSizeInfo(): WindowSizeInfo {
val configuration = LocalConfiguration.current
return WindowSizeInfo(
screenWidthSizeClass = WindowSizeClass.fromWidth(configuration.screenWidthDp),
screenHeightSizeClass = WindowSizeClass.fromHeight(configuration.screenHeightDp),
screenWidth = configuration.screenWidthDp.dp,
screenHeight = configuration.screenHeightDp.dp,
)
}
@Immutable
data class WindowSizeInfo(
val screenWidthSizeClass: WindowSizeClass,
val screenHeightSizeClass: WindowSizeClass,
val screenWidth: Dp,
val screenHeight: Dp,
)

View file

@ -0,0 +1,80 @@
package app.k9mail.core.ui.compose.common.window
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
class WindowSizeClassTest {
@Test
fun `should return compact when width is less than 600`() {
val width = 599
val windowSizeClass = WindowSizeClass.fromWidth(width)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Compact)
}
@Test
fun `should return medium when width is 600`() {
val width = 600
val windowSizeClass = WindowSizeClass.fromWidth(width)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Medium)
}
@Test
fun `should return medium when width is less than 840`() {
val width = 839
val windowSizeClass = WindowSizeClass.fromWidth(width)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Medium)
}
@Test
fun `should return expanded when width is 840`() {
val width = 840
val windowSizeClass = WindowSizeClass.fromWidth(width)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Expanded)
}
@Test
fun `should return compact when height is less than 480`() {
val height = 479
val windowSizeClass = WindowSizeClass.fromHeight(height)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Compact)
}
@Test
fun `should return medium when height is 480`() {
val height = 480
val windowSizeClass = WindowSizeClass.fromHeight(height)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Medium)
}
@Test
fun `should return medium when height is less than 900`() {
val height = 899
val windowSizeClass = WindowSizeClass.fromHeight(height)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Medium)
}
@Test
fun `should return expanded when height is 900`() {
val height = 900
val windowSizeClass = WindowSizeClass.fromHeight(height)
assertThat(windowSizeClass).isEqualTo(WindowSizeClass.Expanded)
}
}

View file

@ -0,0 +1,38 @@
## Core - UI - Compose - Design system
Uses [`:core:ui:compose:theme`](../theme/README.md)
## Background
[Jetpack Compose](https://developer.android.com/jetpack/compose) is a declarative UI toolkit for Android that provides a modern and efficient way to build UIs for Android apps. In this context, design systems and atomic design can help designers and developers create more scalable, maintainable, and reusable UIs.
### Design system
A design system is a collection of guidelines, principles, and tools that help teams create consistent and cohesive visual designs and user experiences.
It typically includes a set of reusable components, such as icons, typography, color palettes, and layouts, that can be combined and customized to create new designs.
The design system also provides documentation and resources for designers and developers to ensure that the designs are implemented consistently and efficiently across all platforms and devices.
The goal of a design system is to streamline the design process, improve design quality, and maintain brand consistency.
An example is Google's [Material Design](https://m3.material.io/) that is used to develop cohesive apps.
### Atomic Design
![Atomic design](assets/images/atomic_design.svg)
Atomic design is a methodology for creating user interfaces (UI) in a design system by breaking them down into smaller, reusable components.
These components are classified into five categories based on their level of abstraction: **atoms**, **molecules**, **organisms**, **templates**, and **pages**.
- **Atoms** are the smallest building blocks, such as buttons, labels, and input fields and could be combined to create more complex components.
- **Molecules** are groups of atoms that work together, like search bars, forms or menus
- **Organisms** are more complex components that combine molecules and atoms, such as headers or cards.
- **Templates** are pages with placeholders for components
- **Pages** are the final UI
By using atomic design, designers and developers can create more consistent and reusable UIs.
This can save time and improve the overall quality, as well as facilitate collaboration between team members.
## Acknowledgement
- [Atomic Design Methodology | Atomic Design by Brad Frost](https://atomicdesign.bradfrost.com/chapter-2/)
- [Atomic Design: Getting Started | Blog | We Are Mobile First](https://www.wearemobilefirst.com/blog/atomic-design)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,16 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "app.k9mail.core.ui.compose.designsystem"
resourcePrefix = "designsystem_"
}
dependencies {
api(projects.core.ui.compose.theme)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material.icons.extended)
testImplementation(projects.core.ui.compose.testing)
}

View file

@ -0,0 +1,32 @@
package app.k9mail.core.ui.compose.designsystem.atom
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Surface as MaterialSurface
@Composable
fun Background(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
MaterialSurface(
modifier = modifier,
content = content,
color = MainTheme.colors.background,
)
}
@Preview(showBackground = true)
@Composable
internal fun BackgroundPreview() {
PreviewWithThemes {
Background(
modifier = Modifier.fillMaxSize(),
content = {},
)
}
}

View file

@ -0,0 +1,45 @@
package app.k9mail.core.ui.compose.designsystem.atom
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Checkbox as MaterialCheckbox
@Composable
fun Checkbox(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
MaterialCheckbox(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled,
)
}
@Preview(showBackground = true)
@Composable
internal fun CheckboxPreview() {
PreviewWithThemes {
Checkbox(
checked = true,
onCheckedChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun CheckboxDisabledPreview() {
PreviewWithThemes {
Checkbox(
checked = true,
onCheckedChange = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,37 @@
package app.k9mail.core.ui.compose.designsystem.atom
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Surface as MaterialSurface
@Composable
fun Surface(
modifier: Modifier = Modifier,
color: Color = MainTheme.colors.surface,
elevation: Dp = MainTheme.elevations.default,
content: @Composable () -> Unit,
) {
MaterialSurface(
modifier = modifier,
content = content,
elevation = elevation,
color = color,
)
}
@Preview(showBackground = true)
@Composable
internal fun SurfacePreview() {
PreviewWithThemes {
Surface(
modifier = Modifier.fillMaxSize(),
content = {},
)
}
}

View file

@ -0,0 +1,52 @@
package app.k9mail.core.ui.compose.designsystem.atom.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.atom.text.TextButton
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Button as MaterialButton
@Composable
fun Button(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
) {
MaterialButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors(),
contentPadding = contentPadding,
) {
TextButton(text = text)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonPreview() {
PreviewWithThemes {
Button(
text = "Button",
onClick = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonDisabledPreview() {
PreviewWithThemes {
Button(
text = "ButtonDisabled",
onClick = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,65 @@
package app.k9mail.core.ui.compose.designsystem.atom.button
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.atom.text.TextButton
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedButton as MaterialOutlinedButton
@Composable
fun ButtonOutlined(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
) {
MaterialOutlinedButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.outlinedButtonColors(),
border = BorderStroke(
width = 1.dp,
color = if (enabled) {
MainTheme.colors.primary
} else {
MainTheme.colors.onSurface.copy(
alpha = 0.12f,
)
},
),
contentPadding = contentPadding,
) {
TextButton(text = text)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonOutlinedPreview() {
PreviewWithThemes {
ButtonOutlined(
text = "ButtonOutlined",
onClick = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonOutlinedDisabledPreview() {
PreviewWithThemes {
ButtonOutlined(
text = "ButtonOutlinedDisabled",
onClick = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,52 @@
package app.k9mail.core.ui.compose.designsystem.atom.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.atom.text.TextButton
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.TextButton as MaterialTextButton
@Composable
fun ButtonText(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
) {
MaterialTextButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.textButtonColors(),
contentPadding = contentPadding,
) {
TextButton(text = text)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonTextPreview() {
PreviewWithThemes {
ButtonText(
text = "ButtonText",
onClick = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonTextDisabledPreview() {
PreviewWithThemes {
ButtonText(
text = "ButtonTextDisabled",
onClick = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextBody1(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.body1,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextBody1Preview() {
PreviewWithThemes {
TextBody1(text = "TextBody1")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextBody2(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.body2,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextBody2Preview() {
PreviewWithThemes {
TextBody2(text = "TextBody2")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextButton(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text.uppercase(),
style = MainTheme.typography.button,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextButtonPreview() {
PreviewWithThemes {
TextButton(text = "TextButton")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextCaption(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.caption,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextCaptionPreview() {
PreviewWithThemes {
TextCaption(text = "TextCaption")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline1(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h1,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline1Preview() {
PreviewWithThemes {
TextHeadline1(text = "TextHeadline1")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline2(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h2,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline2Preview() {
PreviewWithThemes {
TextHeadline2(text = "TextHeadline2")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline3(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h3,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline3Preview() {
PreviewWithThemes {
TextHeadline3(text = "TextHeadline3")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline4(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h4,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline4Preview() {
PreviewWithThemes {
TextHeadline4(text = "TextHeadline4")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline5(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h5,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline5Preview() {
PreviewWithThemes {
TextHeadline5(text = "TextHeadline5")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline6(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h6,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline6Preview() {
PreviewWithThemes {
TextHeadline6(text = "TextHeadline6")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextOverline(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text.uppercase(),
style = MainTheme.typography.overline,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextOverlinePreview() {
PreviewWithThemes {
TextOverline(text = "TextOverline")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextSubtitle1(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.subtitle1,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextSubtitle1Preview() {
PreviewWithThemes {
TextSubtitle1(text = "TextSubtitle1")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextSubtitle2(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.subtitle2,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextSubtitle2Preview() {
PreviewWithThemes {
TextSubtitle2(text = "TextSubtitle2")
}
}

View file

@ -0,0 +1,154 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.R
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedTextField as MaterialOutlinedTextField
@Composable
fun PasswordTextFieldOutlined(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: String? = null,
isError: Boolean = false,
) {
var passwordVisibilityState by rememberSaveable { mutableStateOf(false) }
MaterialOutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = selectLabel(label),
trailingIcon = selectTrailingIcon(
isEnabled = enabled,
isPasswordVisible = passwordVisibilityState,
onClick = { passwordVisibilityState = !passwordVisibilityState },
),
isError = isError,
visualTransformation = selectVisualTransformation(
isEnabled = enabled,
isPasswordVisible = passwordVisibilityState,
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
)
}
private fun selectLabel(label: String?): @Composable (() -> Unit)? {
return if (label != null) {
{
Text(text = label)
}
} else {
null
}
}
private fun selectTrailingIcon(
isEnabled: Boolean,
isPasswordVisible: Boolean,
onClick: () -> Unit,
hasTrailingIcon: Boolean = true,
): @Composable (() -> Unit)? {
return if (hasTrailingIcon) {
{
val image = if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
Icons.Filled.Visibility
} else {
Icons.Filled.VisibilityOff
}
val description = if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
stringResource(id = R.string.designsystem_atom_password_textfield_hide_password)
} else {
stringResource(id = R.string.designsystem_atom_password_textfield_show_password)
}
IconButton(onClick = onClick) {
Icon(imageVector = image, contentDescription = description)
}
}
} else {
null
}
}
private fun selectVisualTransformation(
isEnabled: Boolean,
isPasswordVisible: Boolean,
): VisualTransformation {
return if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
}
}
private fun isShowPasswordAllowed(isEnabled: Boolean, isPasswordVisible: Boolean) = isEnabled && isPasswordVisible
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedWithLabelPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
label = "Label",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedDisabledPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
enabled = false,
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedErrorPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
isError = true,
)
}
}

View file

@ -0,0 +1,84 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedTextField as MaterialOutlinedTextField
@Composable
fun TextFieldOutlined(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: String? = null,
isError: Boolean = false,
) {
MaterialOutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = selectLabel(label),
isError = isError,
)
}
private fun selectLabel(label: String?): @Composable (() -> Unit)? {
return if (label != null) {
{
Text(text = label)
}
} else {
null
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedWithLabelPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
label = "Label",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedDisabledPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
enabled = false,
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedErrorPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
isError = true,
)
}
}

View file

@ -0,0 +1,86 @@
package app.k9mail.core.ui.compose.designsystem.template
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.theme.K9Theme
/**
* The [LazyColumnWithFooter] composable creates a [LazyColumn] with a footer.
*
* @param modifier The modifier to be applied to the layout.
* @param verticalArrangement The vertical arrangement of the children.
* @param horizontalAlignment The horizontal alignment of the children.
* @param footer The footer to be displayed at the bottom of the [LazyColumn].
* @param content The content of the [LazyColumn].
*/
@Composable
fun LazyColumnWithFooter(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
footer: @Composable () -> Unit = {},
content: LazyListScope.() -> Unit,
) {
LazyColumn(
modifier = modifier,
verticalArrangement = verticalArrangementWithFooter(verticalArrangement),
horizontalAlignment = horizontalAlignment,
) {
content()
item { footer() }
}
}
@Composable
private fun verticalArrangementWithFooter(verticalArrangement: Arrangement.Vertical) = remember {
object : Arrangement.Vertical {
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) {
val innerSizes = sizes.dropLast(1).toIntArray()
val footerSize = sizes.last()
val innerTotalSize = totalSize - footerSize
with(verticalArrangement) {
arrange(
totalSize = innerTotalSize,
sizes = innerSizes,
outPositions = outPositions,
)
}
outPositions[outPositions.lastIndex] = totalSize - footerSize
}
}
}
@Composable
@Preview
internal fun LazyColumnWithFooterPreview() {
K9Theme {
Surface {
LazyColumnWithFooter(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically),
footer = { Text(text = "Footer") },
) {
items(10) {
Text(text = "Item $it")
}
}
}
}
}

View file

@ -0,0 +1,129 @@
package app.k9mail.core.ui.compose.designsystem.template
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.common.DevicePreviews
import app.k9mail.core.ui.compose.common.window.WindowSizeClass
import app.k9mail.core.ui.compose.common.window.getWindowSizeInfo
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.theme.K9Theme
import app.k9mail.core.ui.compose.theme.MainTheme
/**
* The [ResponsiveContent] composable automatically adapts its child content to different screen sizes and resolutions,
* providing a responsive layout for a better user experience.
*
* It uses the [WindowSizeClass] (Compact, Medium, or Expanded) to make appropriate layout adjustments.
*
* @param modifier The modifier to be applied to the layout.
* @param content The content to be displayed.
*/
@Composable
fun ResponsiveContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val windowSizeClass = getWindowSizeInfo()
when (windowSizeClass.screenWidthSizeClass) {
WindowSizeClass.Compact -> CompactContent(modifier = modifier, content = content)
WindowSizeClass.Medium -> MediumContent(modifier = modifier, content = content)
WindowSizeClass.Expanded -> ExpandedContent(modifier = modifier, content = content)
}
}
@Composable
private fun CompactContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
) {
content()
}
}
@Composable
private fun MediumContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.TopCenter,
) {
Box(
modifier = Modifier.requiredWidth(WindowSizeClass.COMPACT_MAX_WIDTH.dp),
) {
content()
}
}
}
@Composable
private fun ExpandedContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
when (getWindowSizeInfo().screenHeightSizeClass) {
WindowSizeClass.Compact -> MediumContent(modifier, content)
WindowSizeClass.Medium -> {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.TopCenter,
) {
Surface(
modifier = Modifier.requiredWidth(WindowSizeClass.MEDIUM_MAX_WIDTH.dp),
elevation = MainTheme.elevations.raised,
) {
content()
}
}
}
WindowSizeClass.Expanded -> {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.Center,
) {
Surface(
modifier = Modifier
.requiredWidth(WindowSizeClass.MEDIUM_MAX_WIDTH.dp)
.requiredHeight(WindowSizeClass.MEDIUM_MAX_HEIGHT.dp),
elevation = MainTheme.elevations.raised,
) {
content()
}
}
}
}
}
@Composable
@DevicePreviews
internal fun ResponsiveContentPreview() {
K9Theme {
Surface {
ResponsiveContent {
Surface(
color = MainTheme.colors.info,
modifier = Modifier.fillMaxSize(),
) {}
}
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="designsystem_atom_password_textfield_hide_password">Hide password</string>
<string name="designsystem_atom_password_textfield_show_password">Show password</string>
</resources>

View file

@ -0,0 +1,98 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import app.k9mail.core.ui.compose.designsystem.R
import app.k9mail.core.ui.compose.testing.ComposeTest
import org.junit.Test
private const val PASSWORD = "Password input"
class PasswordTextFieldOutlinedKtTest : ComposeTest() {
@Test
fun `should not display password by default`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onNodeWithText(PASSWORD).assertDoesNotExist()
}
@Test
fun `should display password when show password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onNodeWithText(PASSWORD).assertIsDisplayed()
}
@Test
fun `should not display password when hide password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().performClick()
onNodeWithText(PASSWORD).assertDoesNotExist()
}
@Test
fun `should display hide password icon when show password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().assertIsDisplayed()
}
@Test
fun `should display show password icon when hide password icon is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().performClick()
onShowPasswordNode().assertIsDisplayed()
}
private fun SemanticsNodeInteractionsProvider.onShowPasswordNode(): SemanticsNodeInteraction {
return onNodeWithContentDescription(
getString(R.string.designsystem_atom_password_textfield_show_password),
)
}
private fun SemanticsNodeInteractionsProvider.onHidePasswordNode(): SemanticsNodeInteraction {
return onNodeWithContentDescription(
getString(R.string.designsystem_atom_password_textfield_hide_password),
)
}
}

View file

@ -0,0 +1,154 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import app.k9mail.core.ui.compose.testing.ComposeTest
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
private const val VALUE = "Input text"
private const val LABEL = "Label"
data class TextFieldTestData(
val name: String,
val content: @Composable (
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier,
enabled: Boolean?,
label: String?,
) -> Unit,
)
@RunWith(ParameterizedRobolectricTestRunner::class)
class TextFieldKtTest(
data: TextFieldTestData,
) : ComposeTest() {
private val testSubjectName = data.name
private val testSubject = data.content
@Test
fun `should call onValueChange when value changes`() = runComposeTest {
var value = VALUE
setContent {
testSubject(
value = value,
onValueChange = { value = it },
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = null,
)
}
onNodeWithTag(testSubjectName).performClick()
onNodeWithTag(testSubjectName).performTextInput(" + added text")
assertThat(value).isEqualTo("$VALUE + added text")
}
@Test
fun `should be enabled by default`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = null,
)
}
onNodeWithTag(testSubjectName).assertIsEnabled()
}
@Test
fun `should be disabled when enabled is false`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = false,
label = null,
)
}
onNodeWithTag(testSubjectName).assertIsNotEnabled()
}
@Test
fun `should show label when label is not null`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = LABEL,
)
}
onNodeWithText(LABEL).assertIsDisplayed()
}
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun data(): List<TextFieldTestData> = listOf(
TextFieldTestData(
name = "TextFieldOutlined",
content = { value, onValueChange, modifier, enabled, label ->
if (enabled != null) {
TextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = label,
)
} else {
TextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label,
)
}
},
),
TextFieldTestData(
name = "PasswordTextFieldOutlined",
content = { value, onValueChange, modifier, enabled, label ->
if (enabled != null) {
PasswordTextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = label,
)
} else {
PasswordTextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label,
)
}
},
),
)
}
}

View file

@ -0,0 +1,3 @@
## Core - UI - Compose - Testing
Uses [`:core:ui:compose:theme`](../theme/README.md)

View file

@ -0,0 +1,14 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "app.k9mail.core.ui.compose.testing"
}
dependencies {
implementation(projects.core.ui.compose.theme)
implementation(libs.androidx.compose.material)
implementation(libs.bundles.shared.jvm.test.compose)
}

View file

@ -0,0 +1,22 @@
package app.k9mail.core.ui.compose.testing
import androidx.annotation.StringRes
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
open class ComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
fun getString(@StringRes resourceId: Int): String = RuntimeEnvironment.getApplication().getString(resourceId)
fun runComposeTest(testContent: ComposeContentTestRule.() -> Unit): Unit = with(composeTestRule) {
testContent()
}
}

View file

@ -0,0 +1,27 @@
## Core - UI - Compose - Theme
This provides the `MainTheme` with dark/light variation, a wrapper for the Compose Material2 theme. It supports [CompositionLocal](https://developer.android.com/jetpack/compose/compositionlocal) changes to colors, typography, shapes and adds additionally elevations, sizes, spacings and images.
To change Material2 related properties use `MainTheme` instead of `MaterialTheme`:
- `MainTheme.colors`: Material2 colors
- `MainTheme.typography`: Material 2 typography
- `MainTheme.shapes`: Material2 shapes
- `MainTheme.spacings`: Spacings (quarter, half, default, oneHalf, double, triple, quadruple) while default is 8 dp.
- `MainTheme.sizes`: Sizes (smaller, small, medium, large, larger, huge, huger)
- `MainTheme.elevations`: Elevation, e.g. card
- `MainTheme.images`: Images used across the theme, e.g. logo
Included are two derived themes for K-9 and Thunderbird look: `K9Theme` and `ThunderbirdTheme`.
To render previews for both themes use `PreviewWithThemes`. This also includes a dark/light variation:
```
@Preview(showBackground = true)
@Composable
fun MyViewPreview() {
PreviewWithThemes {
MyView()
}
}
```

View file

@ -0,0 +1,13 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "app.k9mail.core.ui.compose.theme"
resourcePrefix = "core_ui_theme_"
}
dependencies {
api(projects.core.ui.compose.common)
implementation(libs.androidx.compose.material)
}

View file

@ -0,0 +1,15 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Immutable
data class Elevations(
val default: Dp = 0.dp,
val raised: Dp = 2.dp,
val card: Dp = 4.dp,
)
internal val LocalElevations = staticCompositionLocalOf { Elevations() }

View file

@ -0,0 +1,14 @@
package app.k9mail.core.ui.compose.theme
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
@Immutable
data class Images(
@DrawableRes val logo: Int,
)
internal val LocalImages = staticCompositionLocalOf<Images> {
error("No LocalImages defined")
}

View file

@ -0,0 +1,38 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import app.k9mail.core.ui.compose.theme.color.MaterialColor
import app.k9mail.core.ui.compose.theme.color.darkColors
import app.k9mail.core.ui.compose.theme.color.lightColors
private val k9LightColorPalette = lightColors(
primary = MaterialColor.gray_800,
primaryVariant = MaterialColor.gray_700,
secondary = MaterialColor.pink_500,
secondaryVariant = MaterialColor.pink_300,
)
private val k9DarkColorPalette = darkColors(
primary = MaterialColor.gray_100,
primaryVariant = MaterialColor.gray_400,
secondary = MaterialColor.pink_300,
secondaryVariant = MaterialColor.pink_500,
)
@Composable
fun K9Theme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val images = Images(logo = R.drawable.core_ui_theme_k9_logo)
MainTheme(
lightColorPalette = k9LightColorPalette,
darkColorPalette = k9DarkColorPalette,
lightImages = images,
darkImages = images,
darkTheme = darkTheme,
content = content,
)
}

View file

@ -0,0 +1,85 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Shapes
import androidx.compose.material.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import app.k9mail.core.ui.compose.theme.color.Colors
import app.k9mail.core.ui.compose.theme.color.LocalColors
import app.k9mail.core.ui.compose.theme.color.toMaterialColors
@Composable
fun MainTheme(
lightColorPalette: Colors,
darkColorPalette: Colors,
lightImages: Images,
darkImages: Images,
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val colors = if (darkTheme) {
darkColorPalette
} else {
lightColorPalette
}
val images = if (darkTheme) {
darkImages
} else {
lightImages
}
CompositionLocalProvider(
LocalColors provides colors,
LocalElevations provides Elevations(),
LocalImages provides images,
LocalSizes provides Sizes(),
LocalSpacings provides Spacings(),
) {
MaterialTheme(
colors = colors.toMaterialColors(),
typography = typography,
shapes = shapes,
content = content,
)
}
}
object MainTheme {
val colors: Colors
@Composable
@ReadOnlyComposable
get() = LocalColors.current
val typography: Typography
@Composable
@ReadOnlyComposable
get() = MaterialTheme.typography
val shapes: Shapes
@Composable
@ReadOnlyComposable
get() = MaterialTheme.shapes
val spacings: Spacings
@Composable
@ReadOnlyComposable
get() = LocalSpacings.current
val sizes: Sizes
@Composable
@ReadOnlyComposable
get() = LocalSizes.current
val elevations: Elevations
@Composable
@ReadOnlyComposable
get() = LocalElevations.current
val images: Images
@Composable
@ReadOnlyComposable
get() = LocalImages.current
}

View file

@ -0,0 +1,64 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
@Composable
fun PreviewWithThemes(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Column(
modifier = modifier,
) {
PreviewHeader(themeName = "K9Theme")
K9Theme {
PreviewSurface(content = content)
}
K9Theme(darkTheme = true) {
PreviewSurface(content = content)
}
PreviewHeader(themeName = "ThunderbirdTheme")
ThunderbirdTheme {
PreviewSurface(content = content)
}
ThunderbirdTheme(darkTheme = true) {
PreviewSurface(content = content)
}
}
}
@Composable
private fun PreviewHeader(
themeName: String,
) {
Surface(
color = Color.Cyan,
) {
Text(
text = themeName,
fontSize = 4.sp,
modifier = Modifier.padding(
start = MainTheme.spacings.half,
end = MainTheme.spacings.half,
),
)
}
}
@Composable
private fun PreviewSurface(
content: @Composable () -> Unit,
) {
Surface(
color = MainTheme.colors.background,
content = content,
)
}

View file

@ -0,0 +1,11 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val shapes = Shapes(
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp),
)

View file

@ -0,0 +1,19 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Immutable
data class Sizes(
val smaller: Dp = 8.dp,
val small: Dp = 16.dp,
val medium: Dp = 32.dp,
val large: Dp = 64.dp,
val larger: Dp = 128.dp,
val huge: Dp = 256.dp,
val huger: Dp = 384.dp,
)
internal val LocalSizes = staticCompositionLocalOf { Sizes() }

View file

@ -0,0 +1,19 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Immutable
data class Spacings(
val quarter: Dp = 2.dp,
val half: Dp = 4.dp,
val default: Dp = 8.dp,
val oneHalf: Dp = 12.dp,
val double: Dp = 16.dp,
val triple: Dp = 24.dp,
val quadruple: Dp = 32.dp,
)
internal val LocalSpacings = staticCompositionLocalOf { Spacings() }

View file

@ -0,0 +1,38 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import app.k9mail.core.ui.compose.theme.color.MaterialColor
import app.k9mail.core.ui.compose.theme.color.darkColors
import app.k9mail.core.ui.compose.theme.color.lightColors
private val thunderbirdLightColorPalette = lightColors(
primary = MaterialColor.blue_800,
primaryVariant = MaterialColor.light_blue_700,
secondary = MaterialColor.pink_500,
secondaryVariant = MaterialColor.pink_300,
)
private val thunderbirdDarkColorPalette = darkColors(
primary = MaterialColor.blue_200,
primaryVariant = MaterialColor.blue_400,
secondary = MaterialColor.pink_300,
secondaryVariant = MaterialColor.pink_500,
)
@Composable
fun ThunderbirdTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val images = Images(logo = R.drawable.core_ui_theme_thunderbird_logo)
MainTheme(
lightColorPalette = thunderbirdLightColorPalette,
darkColorPalette = thunderbirdDarkColorPalette,
lightImages = images,
darkImages = images,
darkTheme = darkTheme,
content = content,
)
}

View file

@ -0,0 +1,123 @@
package app.k9mail.core.ui.compose.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
@Suppress("MagicNumber")
val typography = typographyFromDefaults(
h1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Light,
fontSize = 96.sp,
letterSpacing = (-1.5).sp,
),
h2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Light,
fontSize = 60.sp,
letterSpacing = (-0.5).sp,
),
h3 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 48.sp,
letterSpacing = 0.sp,
),
h4 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 34.sp,
letterSpacing = 0.25.sp,
),
h5 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
letterSpacing = 0.sp,
),
h6 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
letterSpacing = 0.15.sp,
),
subtitle1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
letterSpacing = 0.15.sp,
),
subtitle2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
letterSpacing = 0.1.sp,
),
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
letterSpacing = 0.5.sp,
),
body2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
letterSpacing = 0.25.sp,
),
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
letterSpacing = 1.25.sp,
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
letterSpacing = 0.4.sp,
),
overline = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
letterSpacing = 1.5.sp,
),
)
@Suppress("LongParameterList")
fun typographyFromDefaults(
h1: TextStyle,
h2: TextStyle,
h3: TextStyle,
h4: TextStyle,
h5: TextStyle,
h6: TextStyle,
subtitle1: TextStyle,
subtitle2: TextStyle,
body1: TextStyle,
body2: TextStyle,
button: TextStyle,
caption: TextStyle,
overline: TextStyle,
): Typography {
val defaults = Typography()
return Typography(
h1 = defaults.h1.merge(h1),
h2 = defaults.h2.merge(h2),
h3 = defaults.h3.merge(h3),
h4 = defaults.h4.merge(h4),
h5 = defaults.h5.merge(h5),
h6 = defaults.h6.merge(h6),
subtitle1 = defaults.subtitle1.merge(subtitle1),
subtitle2 = defaults.subtitle2.merge(subtitle2),
body1 = defaults.body1.merge(body1),
body2 = defaults.body2.merge(body2),
button = defaults.button.merge(button),
caption = defaults.caption.merge(caption),
overline = defaults.overline.merge(overline),
)
}

View file

@ -0,0 +1,118 @@
package app.k9mail.core.ui.compose.theme.color
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.material.Colors as MaterialColors
@Immutable
data class Colors(
val primary: Color,
val primaryVariant: Color,
val secondary: Color,
val secondaryVariant: Color,
val background: Color,
val surface: Color,
val success: Color,
val error: Color,
val warning: Color,
val info: Color,
val onPrimary: Color,
val onSecondary: Color,
val onBackground: Color,
val onSurface: Color,
val onMessage: Color,
val isLight: Boolean,
)
@Suppress("LongParameterList")
internal fun lightColors(
primary: Color = MaterialColor.deep_purple_600,
primaryVariant: Color = MaterialColor.deep_purple_900,
secondary: Color = MaterialColor.cyan_600,
secondaryVariant: Color = MaterialColor.cyan_800,
background: Color = Color.White,
surface: Color = Color.White,
success: Color = MaterialColor.green_600,
error: Color = MaterialColor.red_600,
warning: Color = MaterialColor.orange_600,
info: Color = MaterialColor.yellow_600,
onPrimary: Color = Color.White,
onSecondary: Color = Color.Black,
onBackground: Color = Color.Black,
onSurface: Color = Color.Black,
onMessage: Color = Color.White,
) = Colors(
primary = primary,
primaryVariant = primaryVariant,
secondary = secondary,
secondaryVariant = secondaryVariant,
background = background,
surface = surface,
success = success,
error = error,
warning = warning,
info = info,
onPrimary = onPrimary,
onSecondary = onSecondary,
onBackground = onBackground,
onSurface = onSurface,
onMessage = onMessage,
isLight = true,
)
@Suppress("LongParameterList")
internal fun darkColors(
primary: Color = MaterialColor.deep_purple_200,
primaryVariant: Color = MaterialColor.deep_purple_50,
secondary: Color = MaterialColor.cyan_300,
secondaryVariant: Color = secondary,
background: Color = MaterialColor.gray_950,
surface: Color = MaterialColor.gray_950,
success: Color = MaterialColor.green_300,
error: Color = MaterialColor.red_300,
warning: Color = MaterialColor.orange_300,
info: Color = MaterialColor.yellow_300,
onPrimary: Color = Color.Black,
onSecondary: Color = Color.Black,
onBackground: Color = Color.White,
onSurface: Color = Color.White,
onMessage: Color = Color.Black,
) = Colors(
primary = primary,
primaryVariant = primaryVariant,
secondary = secondary,
secondaryVariant = secondaryVariant,
background = background,
surface = surface,
success = success,
error = error,
warning = warning,
info = info,
onPrimary = onPrimary,
onSecondary = onSecondary,
onBackground = onBackground,
onSurface = onSurface,
onMessage = onMessage,
isLight = false,
)
internal fun Colors.toMaterialColors(): MaterialColors {
return MaterialColors(
primary = primary,
primaryVariant = primaryVariant,
secondary = secondary,
secondaryVariant = secondaryVariant,
background = background,
surface = surface,
error = error,
onPrimary = onPrimary,
onSecondary = onSecondary,
onBackground = onBackground,
onSurface = onSurface,
onError = onMessage,
isLight = isLight,
)
}
internal val LocalColors = staticCompositionLocalOf { lightColors() }

View file

@ -0,0 +1,218 @@
@file:Suppress("unused")
package app.k9mail.core.ui.compose.theme.color
import androidx.compose.ui.graphics.Color
internal object MaterialColor {
val red_50 = Color(color = 0xFFFFEBEE)
val red_100 = Color(color = 0xFFFFCDD2)
val red_200 = Color(color = 0xFFEF9A9A)
val red_300 = Color(color = 0xFFE57373)
val red_400 = Color(color = 0xFFEF5350)
val red_500 = Color(color = 0xFFF44336)
val red_600 = Color(color = 0xFFE53935)
val red_700 = Color(color = 0xFFD32F2F)
val red_800 = Color(color = 0xFFC62828)
val red_900 = Color(color = 0xFFB71C1C)
val deep_purple_50 = Color(color = 0xFFEDE7F6)
val deep_purple_100 = Color(color = 0xFFD1C4E9)
val deep_purple_200 = Color(color = 0xFFB39DDB)
val deep_purple_300 = Color(color = 0xFF9575CD)
val deep_purple_400 = Color(color = 0xFF7E57C2)
val deep_purple_500 = Color(color = 0xFF673AB7)
val deep_purple_600 = Color(color = 0xFF5E35B1)
val deep_purple_700 = Color(color = 0xFF512DA8)
val deep_purple_800 = Color(color = 0xFF4527A0)
val deep_purple_900 = Color(color = 0xFF311B92)
val light_blue_50 = Color(color = 0xFFE1F5FE)
val light_blue_100 = Color(color = 0xFFB3E5FC)
val light_blue_200 = Color(color = 0xFF81D4FA)
val light_blue_300 = Color(color = 0xFF4FC3F7)
val light_blue_400 = Color(color = 0xFF29B6F6)
val light_blue_500 = Color(color = 0xFF03A9F4)
val light_blue_600 = Color(color = 0xFF039BE5)
val light_blue_700 = Color(color = 0xFF0288D1)
val light_blue_800 = Color(color = 0xFF0277BD)
val light_blue_900 = Color(color = 0xFF01579B)
val green_50 = Color(color = 0xFFE8F5E9)
val green_100 = Color(color = 0xFFC8E6C9)
val green_200 = Color(color = 0xFFA5D6A7)
val green_300 = Color(color = 0xFF81C784)
val green_400 = Color(color = 0xFF66BB6A)
val green_500 = Color(color = 0xFF4CAF50)
val green_600 = Color(color = 0xFF43A047)
val green_700 = Color(color = 0xFF388E3C)
val green_800 = Color(color = 0xFF2E7D32)
val green_900 = Color(color = 0xFF1B5E20)
val yellow_50 = Color(color = 0xFFFFFDE7)
val yellow_100 = Color(color = 0xFFFFF9C4)
val yellow_200 = Color(color = 0xFFFFF59D)
val yellow_300 = Color(color = 0xFFFFF176)
val yellow_400 = Color(color = 0xFFFFEE58)
val yellow_500 = Color(color = 0xFFFFEB3B)
val yellow_600 = Color(color = 0xFFFDD835)
val yellow_700 = Color(color = 0xFFFBC02D)
val yellow_800 = Color(color = 0xFFF9A825)
val yellow_900 = Color(color = 0xFFF57F17)
val deep_orange_50 = Color(color = 0xFFFBE9E7)
val deep_orange_100 = Color(color = 0xFFFFCCBC)
val deep_orange_200 = Color(color = 0xFFFFAB91)
val deep_orange_300 = Color(color = 0xFFFF8A65)
val deep_orange_400 = Color(color = 0xFFFF7043)
val deep_orange_500 = Color(color = 0xFFFF5722)
val deep_orange_600 = Color(color = 0xFFF4511E)
val deep_orange_700 = Color(color = 0xFFE64A19)
val deep_orange_800 = Color(color = 0xFFD84315)
val deep_orange_900 = Color(color = 0xFFBF360C)
val blue_gray_50 = Color(color = 0xFFECEFF1)
val blue_gray_100 = Color(color = 0xFFCFD8DC)
val blue_gray_200 = Color(color = 0xFFB0BEC5)
val blue_gray_300 = Color(color = 0xFF90A4AE)
val blue_gray_400 = Color(color = 0xFF78909C)
val blue_gray_500 = Color(color = 0xFF607D8B)
val blue_gray_600 = Color(color = 0xFF546E7A)
val blue_gray_700 = Color(color = 0xFF455A64)
val blue_gray_800 = Color(color = 0xFF37474F)
val blue_gray_900 = Color(color = 0xFF263238)
val pink_50 = Color(color = 0xFFFCE4EC)
val pink_100 = Color(color = 0xFFF8BBD0)
val pink_200 = Color(color = 0xFFF48FB1)
val pink_300 = Color(color = 0xFFF06292)
val pink_400 = Color(color = 0xFFEC407A)
val pink_500 = Color(color = 0xFFE91E63)
val pink_600 = Color(color = 0xFFD81B60)
val pink_700 = Color(color = 0xFFC2185B)
val pink_800 = Color(color = 0xFFAD1457)
val pink_900 = Color(color = 0xFF880E4F)
val indigo_50 = Color(color = 0xFFE8EAF6)
val indigo_100 = Color(color = 0xFFC5CAE9)
val indigo_200 = Color(color = 0xFF9FA8DA)
val indigo_300 = Color(color = 0xFF7986CB)
val indigo_400 = Color(color = 0xFF5C6BC0)
val indigo_500 = Color(color = 0xFF3F51B5)
val indigo_600 = Color(color = 0xFF3949AB)
val indigo_700 = Color(color = 0xFF303F9F)
val indigo_800 = Color(color = 0xFF283593)
val indigo_900 = Color(color = 0xFF1A237E)
val cyan_50 = Color(color = 0xFFE0F7FA)
val cyan_100 = Color(color = 0xFFB2EBF2)
val cyan_200 = Color(color = 0xFF80DEEA)
val cyan_300 = Color(color = 0xFF4DD0E1)
val cyan_400 = Color(color = 0xFF26C6DA)
val cyan_500 = Color(color = 0xFF00BCD4)
val cyan_600 = Color(color = 0xFF00ACC1)
val cyan_700 = Color(color = 0xFF0097A7)
val cyan_800 = Color(color = 0xFF00838F)
val cyan_900 = Color(color = 0xFF006064)
val light_green_50 = Color(color = 0xFFF1F8E9)
val light_green_100 = Color(color = 0xFFDCEDC8)
val light_green_200 = Color(color = 0xFFC5E1A5)
val light_green_300 = Color(color = 0xFFAED581)
val light_green_400 = Color(color = 0xFF9CCC65)
val light_green_500 = Color(color = 0xFF8BC34A)
val light_green_600 = Color(color = 0xFF7CB342)
val light_green_700 = Color(color = 0xFF689F38)
val light_green_800 = Color(color = 0xFF558B2F)
val light_green_900 = Color(color = 0xFF33691E)
val amber_50 = Color(color = 0xFFFFF8E1)
val amber_100 = Color(color = 0xFFFFECB3)
val amber_200 = Color(color = 0xFFFFE082)
val amber_300 = Color(color = 0xFFFFD54F)
val amber_400 = Color(color = 0xFFFFCA28)
val amber_500 = Color(color = 0xFFFFC107)
val amber_600 = Color(color = 0xFFFFB300)
val amber_700 = Color(color = 0xFFFFA000)
val amber_800 = Color(color = 0xFFFF8F00)
val amber_900 = Color(color = 0xFFFF6F00)
val brown_50 = Color(color = 0xFFEFEBE9)
val brown_100 = Color(color = 0xFFD7CCC8)
val brown_200 = Color(color = 0xFFBCAAA4)
val brown_300 = Color(color = 0xFFA1887F)
val brown_400 = Color(color = 0xFF8D6E63)
val brown_500 = Color(color = 0xFF795548)
val brown_600 = Color(color = 0xFF6D4C41)
val brown_700 = Color(color = 0xFF5D4037)
val brown_800 = Color(color = 0xFF4E342E)
val brown_900 = Color(color = 0xFF3E2723)
val purple_50 = Color(color = 0xFFF3E5F5)
val purple_100 = Color(color = 0xFFE1BEE7)
val purple_200 = Color(color = 0xFFCE93D8)
val purple_300 = Color(color = 0xFFBA68C8)
val purple_400 = Color(color = 0xFFAB47BC)
val purple_500 = Color(color = 0xFF9C27B0)
val purple_600 = Color(color = 0xFF8E24AA)
val purple_700 = Color(color = 0xFF7B1FA2)
val purple_800 = Color(color = 0xFF6A1B9A)
val purple_900 = Color(color = 0xFF4A148C)
val blue_50 = Color(color = 0xFFE3F2FD)
val blue_100 = Color(color = 0xFFBBDEFB)
val blue_200 = Color(color = 0xFF90CAF9)
val blue_300 = Color(color = 0xFF64B5F6)
val blue_400 = Color(color = 0xFF42A5F5)
val blue_500 = Color(color = 0xFF2196F3)
val blue_600 = Color(color = 0xFF1E88E5)
val blue_700 = Color(color = 0xFF1976D2)
val blue_800 = Color(color = 0xFF1565C0)
val blue_900 = Color(color = 0xFF0D47A1)
val teal_50 = Color(color = 0xFFE0F2F1)
val teal_100 = Color(color = 0xFFB2DFDB)
val teal_200 = Color(color = 0xFF80CBC4)
val teal_300 = Color(color = 0xFF4DB6AC)
val teal_400 = Color(color = 0xFF26A69A)
val teal_500 = Color(color = 0xFF009688)
val teal_600 = Color(color = 0xFF00897B)
val teal_700 = Color(color = 0xFF00796B)
val teal_800 = Color(color = 0xFF00695C)
val teal_900 = Color(color = 0xFF004D40)
val lime_50 = Color(color = 0xFFF9FBE7)
val lime_100 = Color(color = 0xFFF0F4C3)
val lime_200 = Color(color = 0xFFE6EE9C)
val lime_300 = Color(color = 0xFFDCE775)
val lime_400 = Color(color = 0xFFD4E157)
val lime_500 = Color(color = 0xFFCDDC39)
val lime_600 = Color(color = 0xFFC0CA33)
val lime_700 = Color(color = 0xFFAFB42B)
val lime_800 = Color(color = 0xFF9E9D24)
val lime_900 = Color(color = 0xFF827717)
val orange_50 = Color(color = 0xFFFFF3E0)
val orange_100 = Color(color = 0xFFFFE0B2)
val orange_200 = Color(color = 0xFFFFCC80)
val orange_300 = Color(color = 0xFFFFB74D)
val orange_400 = Color(color = 0xFFFFA726)
val orange_500 = Color(color = 0xFFFF9800)
val orange_600 = Color(color = 0xFFFB8C00)
val orange_700 = Color(color = 0xFFF57C00)
val orange_800 = Color(color = 0xFFEF6C00)
val orange_900 = Color(color = 0xFFE65100)
val gray_50 = Color(color = 0xFFFAFAFA)
val gray_100 = Color(color = 0xFFF5F5F5)
val gray_200 = Color(color = 0xFFEEEEEE)
val gray_300 = Color(color = 0xFFE0E0E0)
val gray_400 = Color(color = 0xFFBDBDBD)
val gray_500 = Color(color = 0xFF9E9E9E)
val gray_600 = Color(color = 0xFF757575)
val gray_700 = Color(color = 0xFF616161)
val gray_800 = Color(color = 0xFF424242)
val gray_900 = Color(color = 0xFF212121)
val gray_950 = Color(color = 0xFF121212)
}

View file

@ -0,0 +1,104 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group
android:scaleX="0.52411765"
android:scaleY="0.52411765"
android:translateX="45.684708"
android:translateY="44.75294">
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M50,12C46.68,12 44,14.68 44,18V26C44,29.32 46.68,32 50,32H64V48H72V32H74C77.32,32 80,29.32 80,26V18C80,14.68 77.32,12 74,12H50ZM118,12C114.68,12 112,14.68 112,18V26C112,29.32 114.68,32 118,32H120V48H128V32H142C145.32,32 148,29.32 148,26V18C148,14.68 145.32,12 142,12H118ZM32,120V132L57.61,170C59.68,173.59 63.54,176 68,176H124C128.46,176 132.32,173.59 134.39,170H134.4L160,132V120H32Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M50,8C46.68,8 44,10.68 44,14V22C44,25.32 46.68,28 50,28H64V44H72V28H74C77.32,28 80,25.32 80,22V14C80,10.68 77.32,8 74,8H50ZM118,8C114.68,8 112,10.68 112,14V22C112,25.32 114.68,28 118,28H120V44H128V28H142C145.32,28 148,25.32 148,22V14C148,10.68 145.32,8 142,8H118ZM32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V116H32Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M24,116L32,128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128L168,116H24Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#607D8B"
android:pathData="M32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160.01,128V116H32Z" />
<path
android:fillColor="#263238"
android:pathData="M72,16H64V44H72V16Z" />
<path
android:fillColor="#263238"
android:pathData="M128,16H120V44H128V16Z" />
<path
android:fillColor="#4D6570"
android:pathData="M32,127V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V127L134.4,165H134.39C132.32,168.59 128.46,171 124,171H68C63.54,171 59.68,168.59 57.61,165L32,127Z" />
<path
android:fillColor="#607D8B"
android:pathData="M80,22V14C80,10.69 77.31,8 74,8L50,8C46.69,8 44,10.69 44,14V22C44,25.31 46.69,28 50,28L74,28C77.31,28 80,25.31 80,22Z" />
<path
android:fillColor="#607D8B"
android:pathData="M148,22V14C148,10.69 145.31,8 142,8L118,8C114.69,8 112,10.69 112,14V22C112,25.31 114.69,28 118,28L142,28C145.31,28 148,25.31 148,22Z" />
<path
android:fillColor="#4D6570"
android:pathData="M44,21V22C44,25.32 46.68,28 50,28H74C77.32,28 80,25.32 80,22V21C80,24.32 77.32,27 74,27H50C46.68,27 44,24.32 44,21Z" />
<path
android:fillColor="#4D6570"
android:pathData="M112,21V22C112,25.32 114.68,28 118,28H142C145.32,28 148,25.32 148,22V21C148,24.32 145.32,27 142,27H118C114.68,27 112,24.32 112,21Z" />
<path
android:fillColor="#8097A2"
android:pathData="M50,8C46.68,8 44,10.68 44,14V15C44,11.68 46.68,9 50,9H74C77.32,9 80,11.68 80,15V14C80,10.68 77.32,8 74,8H50Z" />
<path
android:fillColor="#8097A2"
android:pathData="M118,8C114.68,8 112,10.68 112,14V15C112,11.68 114.68,9 118,9H142C145.32,9 148,11.68 148,15V14C148,10.68 145.32,8 142,8H118Z" />
<path
android:fillAlpha="0.2"
android:fillColor="#37abc8"
android:pathData="M171.99,120V52C171.99,45.37 166.62,40 159.99,40L31.99,40C25.37,40 19.99,45.37 19.99,52V120C19.99,126.62 25.37,132 31.99,132H159.99C166.62,132 171.99,126.62 171.99,120Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#5fbcd3"
android:pathData="M171.99,116V48C171.99,41.37 166.62,36 159.99,36L31.99,36C25.37,36 19.99,41.37 19.99,48V116C19.99,122.62 25.37,128 31.99,128L159.99,128C166.62,128 171.99,122.62 171.99,116Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#FF2C55"
android:pathData="M172,116V48C172,41.37 166.63,36 160,36L32,36C25.37,36 20,41.37 20,48V116C20,122.63 25.37,128 32,128H160C166.63,128 172,122.63 172,116Z" />
<path
android:fillColor="#00000000"
android:pathData="M36,52L96,84L156,52"
android:strokeWidth="6"
android:strokeColor="#FBE9E7"
android:strokeLineCap="round" />
<path
android:fillColor="#FF2C55"
android:pathData="M32,36C25.35,36 20,41.35 20,48V49C20,42.35 25.35,37 32,37H160C166.65,37 172,42.35 172,49V48C172,41.35 166.65,36 160,36H32Z" />
<path
android:fillColor="#C2185B"
android:pathData="M20,115V116C20,122.65 25.35,128 32,128H160C166.65,128 172,122.65 172,116V115C172,121.65 166.65,127 160,127H32C25.35,127 20,121.65 20,115Z" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M90,156C86.68,156 84,158.68 84,162V174C84,174.27 84.03,174.54 84.06,174.8C84.06,174.8 84.06,174.81 84.06,174.81C84.02,175.2 84,175.6 84,176C84,179.18 85.26,182.23 87.51,184.48C89.77,186.73 92.82,188 96,188C99.18,188 102.24,186.73 104.49,184.48C106.74,182.23 108,179.18 108,176C108,175.61 107.97,175.23 107.93,174.85C107.97,174.57 108,174.29 108,174V162C108,158.67 105.33,156 102,156L90,156Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M90,152C86.68,152 84,154.68 84,158V170C84,170.27 84.03,170.54 84.06,170.8C84.06,170.8 84.06,170.81 84.06,170.81C84.02,171.2 84,171.6 84,172C84,175.18 85.26,178.23 87.51,180.48C89.77,182.73 92.82,184 96,184C99.18,184 102.24,182.73 104.49,180.48C106.74,178.23 108,175.18 108,172C108,171.61 107.97,171.23 107.93,170.85C107.97,170.57 108,170.29 108,170V158C108,154.67 105.33,152 102,152L90,152Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#263238"
android:pathData="M108,170V158C108,154.69 105.31,152 102,152H90C86.69,152 84,154.69 84,158V170C84,173.31 86.69,176 90,176H102C105.31,176 108,173.31 108,170Z" />
<path
android:fillColor="#263238"
android:pathData="M96,184C102.63,184 108,178.63 108,172C108,165.37 102.63,160 96,160C89.37,160 84,165.37 84,172C84,178.63 89.37,184 96,184Z" />
<path
android:fillColor="#37474F"
android:pathData="M90,152C86.68,152 84,154.68 84,158V159C84,155.68 86.68,153 90,153H102C105.32,153 108,155.68 108,159V158C108,154.68 105.32,152 102,152H90Z" />
<path
android:fillColor="#1A252A"
android:pathData="M84.02,171.43C84.01,171.62 84,171.81 84,172C84,175.18 85.26,178.24 87.51,180.49C89.77,182.74 92.82,184 96,184C99.18,184 102.24,182.74 104.49,180.49C106.74,178.24 108,175.18 108,172C108,171.86 107.99,171.73 107.98,171.59C107.83,174.67 106.5,177.57 104.27,179.69C102.04,181.81 99.08,183 96,183C92.89,183 89.91,181.79 87.68,179.63C85.44,177.47 84.13,174.53 84.02,171.43Z" />
</group>
</vector>

View file

@ -0,0 +1,104 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group
android:scaleX="0.52411765"
android:scaleY="0.52411765"
android:translateX="45.684708"
android:translateY="44.75294">
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M50,12C46.68,12 44,14.68 44,18V26C44,29.32 46.68,32 50,32H64V48H72V32H74C77.32,32 80,29.32 80,26V18C80,14.68 77.32,12 74,12H50ZM118,12C114.68,12 112,14.68 112,18V26C112,29.32 114.68,32 118,32H120V48H128V32H142C145.32,32 148,29.32 148,26V18C148,14.68 145.32,12 142,12H118ZM32,120V132L57.61,170C59.68,173.59 63.54,176 68,176H124C128.46,176 132.32,173.59 134.39,170H134.4L160,132V120H32Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M50,8C46.68,8 44,10.68 44,14V22C44,25.32 46.68,28 50,28H64V44H72V28H74C77.32,28 80,25.32 80,22V14C80,10.68 77.32,8 74,8H50ZM118,8C114.68,8 112,10.68 112,14V22C112,25.32 114.68,28 118,28H120V44H128V28H142C145.32,28 148,25.32 148,22V14C148,10.68 145.32,8 142,8H118ZM32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V116H32Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M24,116L32,128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128L168,116H24Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#607D8B"
android:pathData="M32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160.01,128V116H32Z" />
<path
android:fillColor="#263238"
android:pathData="M72,16H64V44H72V16Z" />
<path
android:fillColor="#263238"
android:pathData="M128,16H120V44H128V16Z" />
<path
android:fillColor="#4D6570"
android:pathData="M32,127V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V127L134.4,165H134.39C132.32,168.59 128.46,171 124,171H68C63.54,171 59.68,168.59 57.61,165L32,127Z" />
<path
android:fillColor="#607D8B"
android:pathData="M80,22V14C80,10.69 77.31,8 74,8L50,8C46.69,8 44,10.69 44,14V22C44,25.31 46.69,28 50,28L74,28C77.31,28 80,25.31 80,22Z" />
<path
android:fillColor="#607D8B"
android:pathData="M148,22V14C148,10.69 145.31,8 142,8L118,8C114.69,8 112,10.69 112,14V22C112,25.31 114.69,28 118,28L142,28C145.31,28 148,25.31 148,22Z" />
<path
android:fillColor="#4D6570"
android:pathData="M44,21V22C44,25.32 46.68,28 50,28H74C77.32,28 80,25.32 80,22V21C80,24.32 77.32,27 74,27H50C46.68,27 44,24.32 44,21Z" />
<path
android:fillColor="#4D6570"
android:pathData="M112,21V22C112,25.32 114.68,28 118,28H142C145.32,28 148,25.32 148,22V21C148,24.32 145.32,27 142,27H118C114.68,27 112,24.32 112,21Z" />
<path
android:fillColor="#8097A2"
android:pathData="M50,8C46.68,8 44,10.68 44,14V15C44,11.68 46.68,9 50,9H74C77.32,9 80,11.68 80,15V14C80,10.68 77.32,8 74,8H50Z" />
<path
android:fillColor="#8097A2"
android:pathData="M118,8C114.68,8 112,10.68 112,14V15C112,11.68 114.68,9 118,9H142C145.32,9 148,11.68 148,15V14C148,10.68 145.32,8 142,8H118Z" />
<path
android:fillAlpha="0.2"
android:fillColor="#37abc8"
android:pathData="M171.99,120V52C171.99,45.37 166.62,40 159.99,40L31.99,40C25.37,40 19.99,45.37 19.99,52V120C19.99,126.62 25.37,132 31.99,132H159.99C166.62,132 171.99,126.62 171.99,120Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#5fbcd3"
android:pathData="M171.99,116V48C171.99,41.37 166.62,36 159.99,36L31.99,36C25.37,36 19.99,41.37 19.99,48V116C19.99,122.62 25.37,128 31.99,128L159.99,128C166.62,128 171.99,122.62 171.99,116Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#1E88E5"
android:pathData="M172,116V48C172,41.37 166.63,36 160,36L32,36C25.37,36 20,41.37 20,48V116C20,122.63 25.37,128 32,128H160C166.63,128 172,122.63 172,116Z" />
<path
android:fillColor="#00000000"
android:pathData="M36,52L96,84L156,52"
android:strokeWidth="6"
android:strokeColor="#FBE9E7"
android:strokeLineCap="round" />
<path
android:fillColor="#1E88E5"
android:pathData="M32,36C25.35,36 20,41.35 20,48V49C20,42.35 25.35,37 32,37H160C166.65,37 172,42.35 172,49V48C172,41.35 166.65,36 160,36H32Z" />
<path
android:fillColor="#00796B"
android:pathData="M20,115V116C20,122.65 25.35,128 32,128H160C166.65,128 172,122.65 172,116V115C172,121.65 166.65,127 160,127H32C25.35,127 20,121.65 20,115Z" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M90,156C86.68,156 84,158.68 84,162V174C84,174.27 84.03,174.54 84.06,174.8C84.06,174.8 84.06,174.81 84.06,174.81C84.02,175.2 84,175.6 84,176C84,179.18 85.26,182.23 87.51,184.48C89.77,186.73 92.82,188 96,188C99.18,188 102.24,186.73 104.49,184.48C106.74,182.23 108,179.18 108,176C108,175.61 107.97,175.23 107.93,174.85C107.97,174.57 108,174.29 108,174V162C108,158.67 105.33,156 102,156L90,156Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M90,152C86.68,152 84,154.68 84,158V170C84,170.27 84.03,170.54 84.06,170.8C84.06,170.8 84.06,170.81 84.06,170.81C84.02,171.2 84,171.6 84,172C84,175.18 85.26,178.23 87.51,180.48C89.77,182.73 92.82,184 96,184C99.18,184 102.24,182.73 104.49,180.48C106.74,178.23 108,175.18 108,172C108,171.61 107.97,171.23 107.93,170.85C107.97,170.57 108,170.29 108,170V158C108,154.67 105.33,152 102,152L90,152Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#263238"
android:pathData="M108,170V158C108,154.69 105.31,152 102,152H90C86.69,152 84,154.69 84,158V170C84,173.31 86.69,176 90,176H102C105.31,176 108,173.31 108,170Z" />
<path
android:fillColor="#263238"
android:pathData="M96,184C102.63,184 108,178.63 108,172C108,165.37 102.63,160 96,160C89.37,160 84,165.37 84,172C84,178.63 89.37,184 96,184Z" />
<path
android:fillColor="#37474F"
android:pathData="M90,152C86.68,152 84,154.68 84,158V159C84,155.68 86.68,153 90,153H102C105.32,153 108,155.68 108,159V158C108,154.68 105.32,152 102,152H90Z" />
<path
android:fillColor="#1A252A"
android:pathData="M84.02,171.43C84.01,171.62 84,171.81 84,172C84,175.18 85.26,178.24 87.51,180.49C89.77,182.74 92.82,184 96,184C99.18,184 102.24,182.74 104.49,180.49C106.74,178.24 108,175.18 108,172C108,171.86 107.99,171.73 107.98,171.59C107.83,174.67 106.5,177.57 104.27,179.69C102.04,181.81 99.08,183 96,183C92.89,183 89.91,181.79 87.68,179.63C85.44,177.47 84.13,174.53 84.02,171.43Z" />
</group>
</vector>