Add initial username link screen + QR code generation.
This commit is contained in:
parent
e0c06615fb
commit
855e194baa
30 changed files with 1367 additions and 27 deletions
|
@ -13,7 +13,7 @@ import com.google.zxing.common.BitMatrix;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SquareImageView;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.qr.QrCodeUtil;
|
||||
|
||||
/**
|
||||
* Generates a bitmap asynchronously for the supplied {@link BitMatrix} data and displays it.
|
||||
|
@ -59,7 +59,7 @@ public class QrView extends SquareImageView {
|
|||
}
|
||||
|
||||
public void setQrText(@Nullable String text) {
|
||||
setQrBitmap(QrCode.create(text, foregroundColor, backgroundColor));
|
||||
setQrBitmap(QrCodeUtil.create(text, foregroundColor, backgroundColor));
|
||||
}
|
||||
|
||||
private void setQrBitmap(@Nullable Bitmap qrBitmap) {
|
||||
|
|
|
@ -47,9 +47,19 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
|||
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BioPreference(state.self) {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||
}
|
||||
BioPreference(
|
||||
recipient = state.self,
|
||||
onRowClicked = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||
},
|
||||
onQrButtonClicked = {
|
||||
if (Recipient.self().getUsername().isPresent()) {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
|
||||
} else {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameEducationFragment)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
clickPref(
|
||||
|
@ -216,7 +226,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
|||
}
|
||||
}
|
||||
|
||||
private class BioPreference(val recipient: Recipient, val onClick: () -> Unit) : PreferenceModel<BioPreference>() {
|
||||
private class BioPreference(val recipient: Recipient, val onRowClicked: () -> Unit, val onQrButtonClicked: () -> Unit) : PreferenceModel<BioPreference>() {
|
||||
override fun areContentsTheSame(newItem: BioPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
||||
}
|
||||
|
@ -231,11 +241,12 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
|||
private val avatarView: AvatarImageView = itemView.findViewById(R.id.icon)
|
||||
private val aboutView: TextView = itemView.findViewById(R.id.about)
|
||||
private val badgeView: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||
private val qrButton: View = itemView.findViewById(R.id.qr_button)
|
||||
|
||||
override fun bind(model: BioPreference) {
|
||||
super.bind(model)
|
||||
|
||||
itemView.setOnClickListener { model.onClick() }
|
||||
itemView.setOnClickListener { model.onRowClicked() }
|
||||
|
||||
titleView.text = model.recipient.profileName.toString()
|
||||
summaryView.text = PhoneNumberFormatter.prettyPrint(model.recipient.requireE164())
|
||||
|
@ -246,6 +257,14 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
|||
summaryView.visibility = View.VISIBLE
|
||||
avatarView.visibility = View.VISIBLE
|
||||
|
||||
if (FeatureFlags.usernames()) {
|
||||
qrButton.visibility = View.VISIBLE
|
||||
qrButton.isClickable = true
|
||||
qrButton.setOnClickListener { model.onQrButtonClicked() }
|
||||
} else {
|
||||
qrButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (model.recipient.combinedAboutAndEmoji != null) {
|
||||
aboutView.text = model.recipient.combinedAboutAndEmoji
|
||||
aboutView.visibility = View.VISIBLE
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Shows a QRCode that represents the provided data. Includes a Signal logo in the middle.
|
||||
*/
|
||||
@Composable
|
||||
fun QrCode(
|
||||
data: QrCodeData,
|
||||
modifier: Modifier = Modifier,
|
||||
foregroundColor: Color = Color.Black,
|
||||
backgroundColor: Color = Color.White,
|
||||
deadzonePercent: Float = 0.4f
|
||||
) {
|
||||
val logo = ImageBitmap.imageResource(R.drawable.qrcode_logo)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.drawBehind {
|
||||
drawQr(
|
||||
data = data,
|
||||
foregroundColor = foregroundColor,
|
||||
backgroundColor = backgroundColor,
|
||||
deadzonePercent = deadzonePercent,
|
||||
logo = logo
|
||||
)
|
||||
}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawQr(
|
||||
data: QrCodeData,
|
||||
foregroundColor: Color,
|
||||
backgroundColor: Color,
|
||||
deadzonePercent: Float,
|
||||
logo: ImageBitmap
|
||||
) {
|
||||
// We want an even number of dots on either side of the deadzone
|
||||
val candidateDeadzoneWidth: Int = (data.width * deadzonePercent).toInt()
|
||||
val deadzoneWidth: Int = if ((data.width - candidateDeadzoneWidth) % 2 == 0) {
|
||||
candidateDeadzoneWidth
|
||||
} else {
|
||||
candidateDeadzoneWidth + 1
|
||||
}
|
||||
|
||||
val candidateDeadzoneHeight: Int = (data.height * deadzonePercent).toInt()
|
||||
val deadzoneHeight: Int = if ((data.height - candidateDeadzoneHeight) % 2 == 0) {
|
||||
candidateDeadzoneHeight
|
||||
} else {
|
||||
candidateDeadzoneHeight + 1
|
||||
}
|
||||
|
||||
val deadzoneStartX: Int = (data.width - deadzoneWidth) / 2
|
||||
val deadzoneEndX: Int = deadzoneStartX + deadzoneWidth
|
||||
val deadzoneStartY: Int = (data.height - deadzoneHeight) / 2
|
||||
val deadzoneEndY: Int = deadzoneStartY + deadzoneHeight
|
||||
|
||||
val cellWidthPx: Float = size.width / data.width
|
||||
val cellRadiusPx = cellWidthPx / 2
|
||||
|
||||
for (x in 0 until data.width) {
|
||||
for (y in 0 until data.height) {
|
||||
if (x < deadzoneStartX || x >= deadzoneEndX || y < deadzoneStartY || y >= deadzoneEndY) {
|
||||
drawCircle(
|
||||
color = if (data.get(x, y)) foregroundColor else backgroundColor,
|
||||
radius = cellRadiusPx,
|
||||
center = Offset(x * cellWidthPx + cellRadiusPx, y * cellWidthPx + cellRadiusPx)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logo border
|
||||
val deadzonePaddingPercent = 0.02f
|
||||
val logoBorderRadiusPx = ((deadzonePercent - deadzonePaddingPercent) * size.width) / 2
|
||||
drawCircle(
|
||||
color = foregroundColor,
|
||||
radius = logoBorderRadiusPx,
|
||||
style = Stroke(width = cellWidthPx * 0.7f),
|
||||
center = this.center
|
||||
)
|
||||
|
||||
// Logo
|
||||
val logoWidthPx = ((deadzonePercent / 2) * size.width).toInt()
|
||||
val logoOffsetPx = ((size.width - logoWidthPx) / 2).toInt()
|
||||
drawImage(
|
||||
image = logo,
|
||||
dstOffset = IntOffset(logoOffsetPx, logoOffsetPx),
|
||||
dstSize = IntSize(logoWidthPx, logoWidthPx),
|
||||
colorFilter = ColorFilter.tint(foregroundColor)
|
||||
)
|
||||
|
||||
for (eye in data.eyes()) {
|
||||
val strokeWidth = cellWidthPx
|
||||
|
||||
// Clear the already-drawn dots
|
||||
drawRect(
|
||||
color = backgroundColor,
|
||||
topLeft = Offset(
|
||||
x = eye.position.first * cellWidthPx,
|
||||
y = eye.position.second * cellWidthPx
|
||||
),
|
||||
size = Size(eye.size * cellWidthPx + cellRadiusPx, eye.size * cellWidthPx)
|
||||
)
|
||||
|
||||
// Outer square
|
||||
drawRoundRect(
|
||||
color = foregroundColor,
|
||||
topLeft = Offset(
|
||||
x = eye.position.first * cellWidthPx + strokeWidth / 2,
|
||||
y = eye.position.second * cellWidthPx + strokeWidth / 2
|
||||
),
|
||||
size = Size((eye.size - 1) * cellWidthPx, (eye.size - 1) * cellWidthPx),
|
||||
cornerRadius = CornerRadius(cellRadiusPx * 2, cellRadiusPx * 2),
|
||||
style = Stroke(width = strokeWidth)
|
||||
)
|
||||
|
||||
// Inner square
|
||||
drawRoundRect(
|
||||
color = foregroundColor,
|
||||
topLeft = Offset(
|
||||
x = (eye.position.first + 2) * cellWidthPx,
|
||||
y = (eye.position.second + 2) * cellWidthPx
|
||||
),
|
||||
size = Size((eye.size - 4) * cellWidthPx, (eye.size - 4) * cellWidthPx),
|
||||
cornerRadius = CornerRadius(cellRadiusPx, cellRadiusPx)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
Surface {
|
||||
QrCode(
|
||||
data = QrCodeData.forData("https://signal.org", 64),
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp),
|
||||
deadzonePercent = 0.3f
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
|
||||
/**
|
||||
* Renders a QR code and username as a badge.
|
||||
*/
|
||||
@Composable
|
||||
fun QrCodeBadge(data: QrCodeData?, colorScheme: UsernameQrCodeColorScheme, username: String, modifier: Modifier = Modifier) {
|
||||
val borderColor by animateColorAsState(targetValue = colorScheme.borderColor)
|
||||
val foregroundColor by animateColorAsState(targetValue = colorScheme.foregroundColor)
|
||||
val elevation by animateFloatAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) 10f else 0f)
|
||||
val textColor by animateColorAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) Color.Black else Color.White)
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 59.dp, vertical = 24.dp),
|
||||
color = borderColor,
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
shadowElevation = elevation.dp
|
||||
) {
|
||||
Column {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = 32.dp,
|
||||
start = 40.dp,
|
||||
end = 40.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
.aspectRatio(1f)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White
|
||||
) {
|
||||
if (data != null) {
|
||||
QrCode(
|
||||
data = data,
|
||||
modifier = Modifier.padding(20.dp),
|
||||
foregroundColor = foregroundColor,
|
||||
backgroundColor = Color.White
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = colorScheme.borderColor,
|
||||
modifier = Modifier.size(56.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = username,
|
||||
color = textColor,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 26.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 40.dp,
|
||||
end = 40.dp,
|
||||
bottom = 32.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewWithCode() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
QrCodeBadge(
|
||||
data = QrCodeData.forData("https://signal.org", 64),
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "parker.42"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewWithoutCode() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
QrCodeBadge(
|
||||
data = null,
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "parker.42"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import java.util.BitSet
|
||||
|
||||
/**
|
||||
* Efficient representation of raw QR code data. Stored as an X/Y grid of points, where (0, 0) is the top left corner.
|
||||
* X increases as you move right, and Y increases as you go down.
|
||||
*/
|
||||
class QrCodeData(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
private val bits: BitSet
|
||||
) {
|
||||
|
||||
fun get(x: Int, y: Int): Boolean {
|
||||
return bits.get(y * width + x)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the "eyes" of the QR code -- the big squares in the three corners.
|
||||
*/
|
||||
fun eyes(): List<Eye> {
|
||||
val eyes: MutableList<Eye> = mutableListOf()
|
||||
|
||||
val size: Int = getPossibleEyeSize()
|
||||
|
||||
// Top left
|
||||
if (
|
||||
horizontalLineExists(0, 0, size) &&
|
||||
horizontalLineExists(0, size - 1, size) &&
|
||||
verticalLineExists(0, 0, size) &&
|
||||
verticalLineExists(size - 1, 0, size)
|
||||
) {
|
||||
eyes += Eye(
|
||||
position = 0 to 0,
|
||||
size = size
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom left
|
||||
if (
|
||||
horizontalLineExists(0, height - size, size) &&
|
||||
horizontalLineExists(0, size - 1, size) &&
|
||||
verticalLineExists(0, height - size, size) &&
|
||||
verticalLineExists(size - 1, height - size, size)
|
||||
) {
|
||||
eyes += Eye(
|
||||
position = 0 to height - size,
|
||||
size = size
|
||||
)
|
||||
}
|
||||
|
||||
// Top right
|
||||
if (
|
||||
horizontalLineExists(width - size, 0, size) &&
|
||||
horizontalLineExists(width - size, size - 1, size) &&
|
||||
verticalLineExists(width - size, 0, size) &&
|
||||
verticalLineExists(width - 1, 0, size)
|
||||
) {
|
||||
eyes += Eye(
|
||||
position = width - size to 0,
|
||||
size = size
|
||||
)
|
||||
}
|
||||
|
||||
return eyes
|
||||
}
|
||||
|
||||
private fun getPossibleEyeSize(): Int {
|
||||
var x = 0
|
||||
|
||||
while (get(x, 0)) {
|
||||
x++
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
private fun horizontalLineExists(x: Int, y: Int, length: Int): Boolean {
|
||||
for (p in x until x + length) {
|
||||
if (!get(p, y)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun verticalLineExists(x: Int, y: Int, length: Int): Boolean {
|
||||
for (p in y until y + length) {
|
||||
if (!get(x, p)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
data class Eye(
|
||||
val position: Pair<Int, Int>,
|
||||
val size: Int
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Converts the provided string data into a QR representation.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun forData(data: String, size: Int): QrCodeData {
|
||||
val qrCodeWriter = QRCodeWriter()
|
||||
val hints = mapOf(EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.H.toString())
|
||||
|
||||
val padded = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, size, size, hints)
|
||||
val dimens = padded.enclosingRectangle
|
||||
val xStart = dimens[0]
|
||||
val yStart = dimens[1]
|
||||
val width = dimens[2]
|
||||
val height = dimens[3]
|
||||
val bitSet = BitSet(width * height)
|
||||
|
||||
for (x in xStart until xStart + width) {
|
||||
for (y in yStart until yStart + height) {
|
||||
if (padded.get(x, y)) {
|
||||
val destX = x - xStart
|
||||
val destY = y - yStart
|
||||
bitSet.set(destY * width + destX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QrCodeData(width, height, bitSet)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* A set of color schemes for sharing QR codes.
|
||||
*/
|
||||
enum class UsernameQrCodeColorScheme(
|
||||
val borderColor: Color,
|
||||
val foregroundColor: Color,
|
||||
private val key: String
|
||||
) {
|
||||
Blue(
|
||||
borderColor = Color(0xFF506ECD),
|
||||
foregroundColor = Color(0xFF2449C0),
|
||||
key = "blue"
|
||||
),
|
||||
White(
|
||||
borderColor = Color(0xFFFFFFFF),
|
||||
foregroundColor = Color(0xFF464852),
|
||||
key = "white"
|
||||
),
|
||||
Grey(
|
||||
borderColor = Color(0xFF6A6C74),
|
||||
foregroundColor = Color(0xFF464852),
|
||||
key = "grey"
|
||||
),
|
||||
Tan(
|
||||
borderColor = Color(0xFFBBB29A),
|
||||
foregroundColor = Color(0xFF73694F),
|
||||
key = "tan"
|
||||
),
|
||||
Green(
|
||||
borderColor = Color(0xFF97AA89),
|
||||
foregroundColor = Color(0xFF55733F),
|
||||
key = "green"
|
||||
),
|
||||
Orange(
|
||||
borderColor = Color(0xFFDE7134),
|
||||
foregroundColor = Color(0xFFDA6C2E),
|
||||
key = "orange"
|
||||
),
|
||||
Pink(
|
||||
borderColor = Color(0xFFEA7B9D),
|
||||
foregroundColor = Color(0xFFBB617B),
|
||||
key = "pink"
|
||||
),
|
||||
Purple(
|
||||
borderColor = Color(0xFF9E7BE9),
|
||||
foregroundColor = Color(0xFF7651C5),
|
||||
key = "purple"
|
||||
);
|
||||
|
||||
fun serialize(): String {
|
||||
return key
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns the [UsernameQrCodeColorScheme] based on the serialized string. If no match is found, the default of [Blue] is returned.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deserialize(serialized: String?): UsernameQrCodeColorScheme {
|
||||
return values().firstOrNull { it.key == serialized } ?: Blue
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.colorpicker
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeBadge
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
||||
/**
|
||||
* Gives the user the ability to change the color of their shareable username QR code with a live preview.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
class UsernameLinkQrColorPickerFragment : ComposeFragment() {
|
||||
|
||||
val viewModel: UsernameLinkQrColorPickerViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state: UsernameLinkQrColorPickerState by viewModel.state
|
||||
val navController: NavController by remember { mutableStateOf(findNavController()) }
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopAppBarContent(onBackClicked = { navController.popBackStack() }) }
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
QrCodeBadge(
|
||||
data = state.qrCodeData,
|
||||
colorScheme = state.selectedColorScheme,
|
||||
username = state.username
|
||||
)
|
||||
|
||||
ColorPicker(
|
||||
colors = state.colorSchemes,
|
||||
selected = state.selectedColorScheme,
|
||||
onSelectionChanged = { color -> viewModel.onColorSelected(color) }
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f, false)
|
||||
.fillMaxWidth()
|
||||
.padding(end = 24.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Buttons.MediumTonal(onClick = { navController.popBackStack() }) {
|
||||
Text(stringResource(R.string.UsernameLinkSettings_done_button_label))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopAppBarContent(onBackClicked: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.UsernameLinkSettings_color_picker_app_bar_title))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
Image(painter = painterResource(R.drawable.symbol_arrow_left_24), contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorPicker(colors: ImmutableList<UsernameQrCodeColorScheme>, selected: UsernameQrCodeColorScheme, onSelectionChanged: (UsernameQrCodeColorScheme) -> Unit) {
|
||||
LazyVerticalGrid(
|
||||
modifier = Modifier.padding(horizontal = 30.dp),
|
||||
columns = GridCells.Adaptive(minSize = 88.dp)
|
||||
) {
|
||||
colors.forEach { color ->
|
||||
item(key = color.serialize()) {
|
||||
ColorPickerItem(
|
||||
color = color,
|
||||
selected = color == selected,
|
||||
onClick = {
|
||||
onSelectionChanged(color)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorPickerItem(color: UsernameQrCodeColorScheme, selected: Boolean, onClick: () -> Unit) {
|
||||
val outerBorderColor by animateColorAsState(targetValue = if (selected) MaterialTheme.colorScheme.onBackground else Color.Transparent)
|
||||
val colorCircleSize by animateFloatAsState(targetValue = if (selected) 44f else 56f)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 13.dp)
|
||||
.border(width = 2.dp, color = outerBorderColor, shape = CircleShape)
|
||||
.size(56.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.border(width = 2.dp, color = Color.Black.copy(alpha = 0.12f), shape = CircleShape)
|
||||
.size(colorCircleSize.dp),
|
||||
shape = CircleShape,
|
||||
color = color.borderColor,
|
||||
content = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ColorPickerItemPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ColorPickerItem(color = UsernameQrCodeColorScheme.Blue, selected = false, onClick = {})
|
||||
ColorPickerItem(color = UsernameQrCodeColorScheme.Blue, selected = true, onClick = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ColorPickerPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
ColorPicker(
|
||||
colors = UsernameQrCodeColorScheme.values().toList().toImmutableList(),
|
||||
selected = UsernameQrCodeColorScheme.Blue,
|
||||
onSelectionChanged = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.colorpicker
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
|
||||
data class UsernameLinkQrColorPickerState(
|
||||
val username: String,
|
||||
val qrCodeData: QrCodeData?,
|
||||
val colorSchemes: ImmutableList<UsernameQrCodeColorScheme>,
|
||||
val selectedColorScheme: UsernameQrCodeColorScheme
|
||||
)
|
|
@ -0,0 +1,57 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.colorpicker
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
|
||||
class UsernameLinkQrColorPickerViewModel : ViewModel() {
|
||||
|
||||
private val username: String = Recipient.self().username.get()
|
||||
|
||||
private val _state = mutableStateOf(
|
||||
UsernameLinkQrColorPickerState(
|
||||
username = username,
|
||||
qrCodeData = null,
|
||||
colorSchemes = UsernameQrCodeColorScheme.values().asList().toImmutableList(),
|
||||
selectedColorScheme = SignalStore.misc().usernameQrCodeColorScheme
|
||||
)
|
||||
)
|
||||
|
||||
val state: State<UsernameLinkQrColorPickerState> = _state
|
||||
|
||||
private val disposable: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
init {
|
||||
disposable += Single
|
||||
.fromCallable { QrCodeData.forData(UsernameUtil.generateLink(username), 64) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData ->
|
||||
_state.value = _state.value.copy(
|
||||
qrCodeData = qrData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
fun onColorSelected(color: UsernameQrCodeColorScheme) {
|
||||
SignalStore.misc().usernameQrCodeColorScheme = color
|
||||
_state.value = _state.value.copy(
|
||||
selectedColorScheme = color
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
|
||||
val viewModel: UsernameLinkSettingsViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
||||
val scope: CoroutineScope = rememberCoroutineScope()
|
||||
val navController: NavController by remember { mutableStateOf(findNavController()) }
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
) { contentPadding ->
|
||||
UsernameLinkShareScreen(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
scope = scope,
|
||||
contentPadding = contentPadding,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.onResume()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewAll() {
|
||||
FragmentContent()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
|
||||
/**
|
||||
* Represents the UI state of the [UsernameLinkSettingsFragment].
|
||||
*/
|
||||
data class UsernameLinkSettingsState(
|
||||
val username: String,
|
||||
val usernameLink: String,
|
||||
val qrCodeData: QrCodeData?,
|
||||
val qrCodeColorScheme: UsernameQrCodeColorScheme
|
||||
)
|
|
@ -0,0 +1,62 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
|
||||
class UsernameLinkSettingsViewModel : ViewModel() {
|
||||
|
||||
private val username: BehaviorSubject<String> = BehaviorSubject.createDefault(Recipient.self().username.get())
|
||||
|
||||
private val _state = mutableStateOf(
|
||||
UsernameLinkSettingsState(
|
||||
username = username.value!!,
|
||||
usernameLink = UsernameUtil.generateLink(username.value!!),
|
||||
qrCodeData = null,
|
||||
qrCodeColorScheme = SignalStore.misc().usernameQrCodeColorScheme
|
||||
)
|
||||
)
|
||||
|
||||
val state: State<UsernameLinkSettingsState> = _state
|
||||
|
||||
private val disposable: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
init {
|
||||
disposable += username
|
||||
.observeOn(Schedulers.io())
|
||||
.map { UsernameUtil.generateLink(it) }
|
||||
.flatMapSingle { generateQrCodeData(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData ->
|
||||
_state.value = _state.value.copy(
|
||||
qrCodeData = qrData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
_state.value = _state.value.copy(
|
||||
qrCodeColorScheme = SignalStore.misc().usernameQrCodeColorScheme
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateQrCodeData(url: String): Single<QrCodeData> {
|
||||
return Single.fromCallable {
|
||||
QrCodeData.forData(url, 64)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeBadge
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* A screen that shows all the data around your username link and how to share it, including a QR code.
|
||||
*/
|
||||
@Composable
|
||||
fun UsernameLinkShareScreen(
|
||||
state: UsernameLinkSettingsState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope,
|
||||
navController: NavController,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
QrCodeBadge(
|
||||
data = state.qrCodeData,
|
||||
colorScheme = state.qrCodeColorScheme,
|
||||
username = state.username
|
||||
)
|
||||
|
||||
ButtonBar(
|
||||
onColorClicked = { navController.safeNavigate(R.id.action_usernameLinkSettingsFragment_to_usernameLinkQrColorPickerFragment) }
|
||||
)
|
||||
|
||||
CopyRow(
|
||||
displayText = state.username,
|
||||
copyMessage = stringResource(R.string.UsernameLinkSettings_username_copied_toast),
|
||||
snackbarHostState = snackbarHostState,
|
||||
scope = scope
|
||||
)
|
||||
|
||||
CopyRow(
|
||||
displayText = state.usernameLink,
|
||||
copyMessage = stringResource(R.string.UsernameLinkSettings_link_copied_toast),
|
||||
snackbarHostState = snackbarHostState,
|
||||
scope = scope
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.UsernameLinkSettings_qr_description),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 36.dp, start = 43.dp, end = 43.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Buttons.Small(onClick = { /*TODO*/ }) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.UsernameLinkSettings_reset_button_label)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ButtonBar(onColorClicked: () -> Unit) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(space = 32.dp, alignment = Alignment.CenterHorizontally),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Buttons.ActionButton(
|
||||
onClick = {},
|
||||
iconResId = R.drawable.symbol_share_android_24,
|
||||
labelResId = R.string.UsernameLinkSettings_share_button_label
|
||||
)
|
||||
Buttons.ActionButton(
|
||||
onClick = onColorClicked,
|
||||
iconResId = R.drawable.symbol_color_24,
|
||||
labelResId = R.string.UsernameLinkSettings_color_button_label
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CopyRow(displayText: String, copyMessage: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
val context = LocalContext.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.clickable {
|
||||
Util.copyToClipboard(context, displayText)
|
||||
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(copyMessage)
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 26.dp, vertical = 16.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.symbol_copy_android_24),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = displayText,
|
||||
modifier = Modifier.padding(start = 26.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme")
|
||||
@Composable
|
||||
private fun ScreenPreviewLightTheme() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
UsernameLinkShareScreen(
|
||||
state = previewState(),
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
scope = rememberCoroutineScope(),
|
||||
navController = NavController(LocalContext.current)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Dark Theme")
|
||||
@Composable
|
||||
private fun ScreenPreviewDarkTheme() {
|
||||
SignalTheme(isDarkMode = true) {
|
||||
Surface {
|
||||
UsernameLinkShareScreen(
|
||||
state = previewState(),
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
scope = rememberCoroutineScope(),
|
||||
navController = NavController(LocalContext.current)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewState(): UsernameLinkSettingsState {
|
||||
val link = UsernameUtil.generateLink("maya.45")
|
||||
return UsernameLinkSettingsState(
|
||||
username = "maya.45",
|
||||
usernameLink = link,
|
||||
qrCodeData = QrCodeData.forData(link, 64),
|
||||
qrCodeColorScheme = UsernameQrCodeColorScheme.Blue
|
||||
)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
/**
|
||||
* A screen that allows you to scan a QR code to start a chat.
|
||||
*/
|
||||
@Composable
|
||||
fun UsernameQrScanScreen(modifier: Modifier = Modifier) {
|
||||
// TODO
|
||||
Text(text = "QR Scanner Placeholder")
|
||||
}
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.keyvalue;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraintObserver;
|
||||
|
||||
|
@ -30,6 +31,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
|||
private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices";
|
||||
private static final String SMS_PHASE_1_START_MS = "misc.sms_export.phase_1_start.3";
|
||||
private static final String LINKED_DEVICES_REMINDER = "misc.linked_devices_reminder";
|
||||
private static final String USERNAME_QR_CODE_COLOR = "mis.username_qr_color_scheme";
|
||||
|
||||
MiscellaneousValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
|
@ -252,4 +254,15 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
|||
public boolean getShouldShowLinkedDevicesReminder() {
|
||||
return getBoolean(LINKED_DEVICES_REMINDER, false);
|
||||
}
|
||||
|
||||
/** The color the user saved for rendering their shareable username QR code. */
|
||||
public @NonNull UsernameQrCodeColorScheme getUsernameQrCodeColorScheme() {
|
||||
String serialized = getString(USERNAME_QR_CODE_COLOR, null);
|
||||
return UsernameQrCodeColorScheme.deserialize(serialized);
|
||||
}
|
||||
|
||||
public void setUsernameQrCodeColorScheme(@NonNull UsernameQrCodeColorScheme color) {
|
||||
putString(USERNAME_QR_CODE_COLOR, color.serialize());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarS
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.NameUtil;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
@ -248,7 +249,7 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
binding.manageProfileUsername.setText(username);
|
||||
|
||||
try {
|
||||
binding.manageProfileUsernameSubtitle.setText(getString(R.string.signal_me_username_url_no_scheme, Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))));
|
||||
binding.manageProfileUsernameSubtitle.setText(UsernameUtil.generateLink(username));
|
||||
} catch (BaseUsernameException e) {
|
||||
Log.w(TAG, "Could not format username link", e);
|
||||
binding.manageProfileUsernameSubtitle.setText(R.string.ManageProfileFragment_your_username);
|
||||
|
|
|
@ -15,12 +15,12 @@ import com.google.zxing.qrcode.QRCodeWriter;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
|
||||
public final class QrCode {
|
||||
public final class QrCodeUtil {
|
||||
|
||||
private QrCode() {
|
||||
private QrCodeUtil() {
|
||||
}
|
||||
|
||||
public static final String TAG = Log.tag(QrCode.class);
|
||||
public static final String TAG = Log.tag(QrCodeUtil.class);
|
||||
|
||||
public static @NonNull Bitmap create(@Nullable String data) {
|
||||
return create(data, Color.BLACK, Color.TRANSPARENT);
|
|
@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.components.qr.QrView;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.qr.QrCodeUtil;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
|
@ -123,7 +123,7 @@ public class GroupLinkShareQrDialogFragment extends DialogFragment {
|
|||
}
|
||||
|
||||
private static Uri createTemporaryPng(@Nullable String url) throws IOException {
|
||||
Bitmap qrBitmap = QrCode.create(url, Color.BLACK, Color.WHITE);
|
||||
Bitmap qrBitmap = QrCodeUtil.create(url, Color.BLACK, Color.WHITE);
|
||||
|
||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
qrBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
|
||||
|
|
|
@ -32,6 +32,10 @@ public class UsernameUtil {
|
|||
private static final Pattern FULL_PATTERN = Pattern.compile(String.format(Locale.US, "^[a-zA-Z_][a-zA-Z0-9_]{%d,%d}$", MIN_LENGTH - 1, MAX_LENGTH - 1), Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DIGIT_START_PATTERN = Pattern.compile("^[0-9].*$");
|
||||
|
||||
|
||||
private static final String BASE_URL_SCHEMELESS = "signal.me/#u/";
|
||||
private static final String BASE_URL = "https://" + BASE_URL_SCHEMELESS;
|
||||
|
||||
public static boolean isValidUsernameForSearch(@Nullable String value) {
|
||||
return !TextUtils.isEmpty(value) && !DIGIT_START_PATTERN.matcher(value).matches();
|
||||
}
|
||||
|
@ -87,6 +91,13 @@ public class UsernameUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static String generateLink(String username) throws BaseUsernameException {
|
||||
byte[] hash = Username.hash(username);
|
||||
String base64 = Base64UrlSafe.encodeBytesWithoutPadding(hash);
|
||||
|
||||
return BASE_URL + base64;
|
||||
}
|
||||
|
||||
public enum InvalidReason {
|
||||
TOO_SHORT, TOO_LONG, INVALID_CHARACTERS, STARTS_WITH_NUMBER
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ import org.thoughtcrime.securesms.database.IdentityTable;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.qr.QrCodeUtil;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -408,7 +408,7 @@ public class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.
|
|||
|
||||
byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized();
|
||||
String qrCodeString = new String(qrCodeData, Charset.forName("ISO-8859-1"));
|
||||
Bitmap qrCodeBitmap = QrCode.create(qrCodeString);
|
||||
Bitmap qrCodeBitmap = QrCodeUtil.create(qrCodeString);
|
||||
|
||||
qrCode.setImageBitmap(qrCodeBitmap);
|
||||
|
||||
|
|
BIN
app/src/main/res/drawable/qrcode_logo.png
Normal file
BIN
app/src/main/res/drawable/qrcode_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
9
app/src/main/res/drawable/symbol_arrow_left_24.xml
Normal file
9
app/src/main/res/drawable/symbol_arrow_left_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3.88 12c0 0.23 0.09 0.45 0.25 0.62l6.5 6.5c0.34 0.34 0.9 0.34 1.24 0 0.34-0.34 0.34-0.9 0-1.24l-5.13-5.13 1.76 0.13h11.25c0.48 0 0.88-0.4 0.88-0.88s-0.4-0.88-0.88-0.88H8.5l-1.76 0.13 5.13-5.13c0.34-0.34 0.34-0.9 0-1.24-0.34-0.34-0.9-0.34-1.24 0l-6.5 6.5C3.97 11.55 3.88 11.77 3.88 12Z"/>
|
||||
</vector>
|
24
app/src/main/res/drawable/symbol_color_24.xml
Normal file
24
app/src/main/res/drawable/symbol_color_24.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6 15.5C6 14.67 6.67 14 7.5 14S9 14.67 9 15.5 8.33 17 7.5 17 6 16.33 6 15.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17.63 9.25c-0.83 0-1.5 0.67-1.5 1.5s0.67 1.5 1.5 1.5c0.82 0 1.5-0.67 1.5-1.5s-0.68-1.5-1.5-1.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4.88 10.75c0-0.83 0.67-1.5 1.5-1.5 0.82 0 1.5 0.67 1.5 1.5s-0.68 1.5-1.5 1.5c-0.83 0-1.5-0.67-1.5-1.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.5 5.38c-0.83 0-1.5 0.67-1.5 1.5 0 0.82 0.67 1.5 1.5 1.5S16 7.7 16 6.87c0-0.83-0.67-1.5-1.5-1.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8 6.88c0-0.83 0.67-1.5 1.5-1.5S11 6.05 11 6.88c0 0.82-0.67 1.5-1.5 1.5S8 7.7 8 6.87Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 1.13C6 1.13 1.12 5.99 1.12 12 1.13 18 6 22.87 12 22.88c1.23 0 2.24-1.05 2.19-2.28-0.02-0.65-0.33-1.13-0.52-1.4-0.01-0.04-0.03-0.06-0.05-0.08-0.18-0.3-0.27-0.47-0.27-0.75 0-0.73 0.5-1.28 1.23-1.28h2.05c3.36-0.01 6.25-2.38 6.25-6.25 0-2.59-1.38-5.02-3.38-6.78-2-1.77-4.7-2.93-7.5-2.93ZM2.87 12c0-5.04 4.09-9.13 9.13-9.13 2.33 0 4.63 0.98 6.34 2.5 1.73 1.52 2.79 3.5 2.79 5.47 0 2.8-2.01 4.49-4.5 4.5h-2.05c-1.8 0-2.98 1.44-2.98 3.03 0 0.8 0.32 1.33 0.55 1.7 0.25 0.37 0.29 0.45 0.3 0.58v0.02c0 0.24-0.21 0.46-0.45 0.45-5.04 0-9.13-4.08-9.13-9.12Z"/>
|
||||
</vector>
|
|
@ -35,12 +35,13 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/qr_button"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginEnd="24dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/title"
|
||||
|
@ -72,4 +73,19 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/qr_button"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:icon="@drawable/symbol_qrcode_24"
|
||||
app:iconSize="20dp"
|
||||
app:iconTint="@color/core_black"
|
||||
app:backgroundTint="@color/signal_light_colorSurface3"
|
||||
style="@style/Widget.Signal.Button.Icon"/>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -10,6 +10,26 @@
|
|||
android:name="org.thoughtcrime.securesms.components.settings.app.AppSettingsFragment"
|
||||
android:label="app_settings_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_usernameLinkSettingsFragment"
|
||||
app:destination="@id/usernameLinkSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_usernameEducationFragment"
|
||||
app:destination="@id/manageProfileActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
<argument
|
||||
android:name="start_at_username"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="true"
|
||||
/>
|
||||
</action>
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_accountSettingsFragment"
|
||||
app:destination="@id/accountSettingsFragment"
|
||||
|
@ -781,6 +801,23 @@
|
|||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/usernameLinkSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsFragment" >
|
||||
|
||||
<action
|
||||
android:id="@+id/action_usernameLinkSettingsFragment_to_usernameLinkQrColorPickerFragment"
|
||||
app:destination="@id/usernameLinkQrColorPickerFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/usernameLinkQrColorPickerFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.usernamelinks.colorpicker.UsernameLinkQrColorPickerFragment" />
|
||||
|
||||
<include app:graph="@navigation/story_privacy_settings" />
|
||||
|
||||
</navigation>
|
|
@ -5963,5 +5963,28 @@
|
|||
<!-- Displayed in a text row, allowing the user to delete the call link -->
|
||||
<string name="CallLinkDetailsFragment__delete_call_link">Delete call link</string>
|
||||
|
||||
<!-- Button label for the share button in the username link settings -->
|
||||
<string name="UsernameLinkSettings_share_button_label">Share</string>
|
||||
<!-- Button label for the color selector button in the username link settings -->
|
||||
<string name="UsernameLinkSettings_color_button_label">Color</string>
|
||||
<!-- Description text for QR code and links in the username link settings -->
|
||||
<string name="UsernameLinkSettings_qr_description">Only share your QR code and link with people you trust. When shared others will be able to see your username and start a chat with you.</string>
|
||||
<!-- Content of a toast that will show after the username is copied to the clipboard -->
|
||||
<string name="UsernameLinkSettings_username_copied_toast">Username copied</string>
|
||||
<!-- Content of a toast that will show after the username link is copied to the clipboard -->
|
||||
<string name="UsernameLinkSettings_link_copied_toast">Link copied</string>
|
||||
<!-- Button label for a button that will reset your username and give you a new link -->
|
||||
<string name="UsernameLinkSettings_reset_button_label">Reset</string>
|
||||
<!-- Button label for a button that indicates that the user is done changing the current setting -->
|
||||
<string name="UsernameLinkSettings_done_button_label">Done</string>
|
||||
<!-- Label for a tab that shows a screen to view your username QR code -->
|
||||
<string name="UsernameLinkSettings_code_tab_name">Code</string>
|
||||
<!-- Label for a tab that shows a screen to scan a QR code -->
|
||||
<string name="UsernameLinkSettings_scan_tab_name">Scan</string>
|
||||
<!-- Description text shown underneath the username QR code scanner -->
|
||||
<string name="UsernameLinkSettings_qr_scan_description">Scan the QR Code on your contact’s device.</string>
|
||||
<!-- App bar title for the username QR code color picker screen -->
|
||||
<string name="UsernameLinkSettings_color_picker_app_bar_title">Color</string>
|
||||
|
||||
<!-- EOF -->
|
||||
</resources>
|
||||
|
|
|
@ -429,21 +429,25 @@
|
|||
</attr>
|
||||
</declare-styleable>
|
||||
|
||||
<style name="Widget.Signal.Button.Icon.Circular" parent="Widget.MaterialComponents.Button.Icon">
|
||||
<item name="android:layout_width">48dp</item>
|
||||
<item name="android:layout_height">48dp</item>
|
||||
<item name="iconSize">24dp</item>
|
||||
<style name="Widget.Signal.Button.Icon" parent="Widget.Material3.Button.IconButton">
|
||||
<item name="android:elevation" tools:ignore="NewApi">0dp</item>
|
||||
<item name="android:stateListAnimator" tools:ignore="NewApi">@null</item>
|
||||
<item name="android:insetRight">0dp</item>
|
||||
<item name="android:insetLeft">0dp</item>
|
||||
<item name="android:insetTop">0dp</item>
|
||||
<item name="android:insetBottom">0dp</item>
|
||||
<item name="iconGravity">textStart</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Signal.Button.Icon.Circular" parent="Widget.Signal.Button.Icon">
|
||||
<item name="android:layout_width">48dp</item>
|
||||
<item name="android:layout_height">48dp</item>
|
||||
<item name="iconSize">24dp</item>
|
||||
<item name="android:elevation" tools:ignore="NewApi">0dp</item>
|
||||
<item name="android:stateListAnimator" tools:ignore="NewApi">@null</item>
|
||||
<item name="iconTint">@color/white</item>
|
||||
<item name="iconTintMode">multiply</item>
|
||||
<item name="iconPadding">0dp</item>
|
||||
<item name="rippleColor">@color/core_ultramarine</item>
|
||||
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Signal.Button.Rounded</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Signal.Button.Icon.Circular.Small">
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package org.signal.core.ui.theme
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
|
@ -9,28 +11,32 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val typography = Typography().run {
|
||||
copy(
|
||||
headlineLarge = headlineLarge.copy(
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = headlineMedium.copy(
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleLarge = titleLarge.copy(
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleMedium = titleMedium.copy(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontStyle = FontStyle.Normal,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.0125.sp
|
||||
letterSpacing = 0.0125.sp,
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontStyle = FontStyle.Normal
|
||||
),
|
||||
titleSmall = titleSmall.copy(
|
||||
fontSize = 16.sp,
|
||||
|
@ -38,10 +44,12 @@ private val typography = Typography().run {
|
|||
letterSpacing = 0.0125.sp
|
||||
),
|
||||
bodyLarge = bodyLarge.copy(
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 22.sp,
|
||||
letterSpacing = 0.0125.sp
|
||||
),
|
||||
bodyMedium = bodyMedium.copy(
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.0107.sp
|
||||
),
|
||||
|
@ -51,6 +59,7 @@ private val typography = Typography().run {
|
|||
letterSpacing = 0.0192.sp
|
||||
),
|
||||
labelLarge = labelLarge.copy(
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.0107.sp
|
||||
),
|
||||
|
@ -60,6 +69,7 @@ private val typography = Typography().run {
|
|||
letterSpacing = 0.0192.sp
|
||||
),
|
||||
labelSmall = labelSmall.copy(
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.025.sp
|
||||
)
|
||||
|
@ -172,6 +182,63 @@ fun SignalTheme(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun TypographyPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Headline Small",
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
Text(
|
||||
text = "Headline Small",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
Text(
|
||||
text = "Headline Small",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Text(
|
||||
text = "Title Large",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = "Title Medium",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "Title Small",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = "Body Large",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = "Body Medium",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "Body Small",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "Label Large",
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
Text(
|
||||
text = "Label Medium",
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Text(
|
||||
text = "Label Small",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SignalTheme {
|
||||
val colors: ExtendedColors
|
||||
@Composable
|
||||
|
|
|
@ -139,6 +139,7 @@ dependencyResolutionManagement {
|
|||
alias('lottie').to('com.airbnb.android:lottie:5.2.0')
|
||||
alias('dnsjava').to('dnsjava:dnsjava:2.1.9')
|
||||
alias('nanohttpd-webserver').to('org.nanohttpd:nanohttpd-webserver:2.3.1')
|
||||
alias('kotlinx-collections-immutable').to('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5')
|
||||
|
||||
// Can't use the newest version because it hits some weird NoClassDefFoundException
|
||||
alias('jknack-handlebars').to('com.github.jknack:handlebars:4.0.7')
|
||||
|
|
|
@ -44,6 +44,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="30fd58f97339dde1f7f779b1b6a448c13f65102de46bacd5cc7849b762a4e7d2" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.activity" name="activity-compose" version="1.5.1">
|
||||
<artifact name="activity-compose-1.5.1.aar">
|
||||
<sha256 value="8374138f15251cc3ed375425599e94a36038c05cf877f877281ae019b95b844a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="activity-compose-1.5.1.module">
|
||||
<sha256 value="10a44d247a4555af19e3bb8e06b5f24c4ed72252c5feace8de244a35ece256f7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.activity" name="activity-ktx" version="1.5.1">
|
||||
<artifact name="activity-ktx-1.5.1.aar">
|
||||
<sha256 value="fd69a5ccb99244cb7c5224580a58e23238d10ed4086199a33e9bfc31c4e4834f" origin="Generated by Gradle"/>
|
||||
|
@ -948,6 +956,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="01e413b73cbe38cb714dc5bdb21bd860931124c8e5f2369803f4aacc49081c9f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel-compose" version="2.5.1">
|
||||
<artifact name="lifecycle-viewmodel-compose-2.5.1.aar">
|
||||
<sha256 value="4b0f50cca837753d82942822b996c214f09c8c709524f1cc95e03facd96bf466" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="lifecycle-viewmodel-compose-2.5.1.module">
|
||||
<sha256 value="9453b61679d8d5c53b8ea3d61444239cc0c9e49b796e204f48fd1f0332ca2686" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel-ktx" version="2.5.1">
|
||||
<artifact name="lifecycle-viewmodel-ktx-2.5.1.aar">
|
||||
<sha256 value="30eecb351d81f0c429e186e65a892a42ce1d5bc5c80420bfece4ae279333023d" origin="Generated by Gradle"/>
|
||||
|
@ -1003,6 +1019,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="459b2d0214420b6df2edb448c8a7c226dbecdb98b797685eb3da5f0a3b0adcbd" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation" name="navigation-compose" version="2.5.3">
|
||||
<artifact name="navigation-compose-2.5.3.aar">
|
||||
<sha256 value="85b5fee5718aa0ce736f4e6a3aca64862a1a1093f619b3e173786712912118d4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="navigation-compose-2.5.3.module">
|
||||
<sha256 value="a90d53ce3a46c56c87a866f118c4d7bf9cb90bb9a5c3e83ce92bb3cbfd805f14" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation" name="navigation-fragment" version="2.5.3">
|
||||
<artifact name="navigation-fragment-2.5.3.aar">
|
||||
<sha256 value="8fd447ce032b1850bcded21855061d5dd209bf564dffb3a89451d0e642b26bec" origin="Generated by Gradle"/>
|
||||
|
|
Loading…
Add table
Reference in a new issue