feat(IconOptions): custom image preview

This commit is contained in:
rushiiMachine 2025-05-31 12:10:05 -07:00
parent 961cd93a6a
commit 71bf74c880
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
3 changed files with 117 additions and 46 deletions

View file

@ -1,10 +1,10 @@
package com.aliucord.manager.ui.components
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.*
import android.graphics.drawable.*
import android.os.Build
import android.widget.ImageView
import androidx.annotation.RequiresApi
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -17,8 +17,9 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.graphics.drawable.toDrawable
import com.aliucord.manager.R
import com.aliucord.manager.ui.screens.patchopts.PatchOptions.IconReplacement
@Composable
fun discordIconDrawable(
@ -30,7 +31,6 @@ fun discordIconDrawable(
val context = LocalContext.current
val size = with(density) { size.roundToPx() }
val bitmap = remember(size) { createBitmap(size, size) }
val drawable = remember(context, oldLogo) {
val drawableId = when (oldLogo) {
false -> R.drawable.ic_discord
@ -45,6 +45,7 @@ fun discordIconDrawable(
}
}
val bitmap = remember(size) { createBitmap(size, size) }
DisposableEffect(size) {
onDispose {
bitmap.recycle()
@ -57,8 +58,8 @@ fun discordIconDrawable(
style = Paint.Style.FILL
setColor(backgroundColor.toArgb())
}
drawRect(Rect(0, 0, width, height), paint)
drawable.setBounds(0, 0, width, height)
drawable.draw(this)
}
@ -67,15 +68,30 @@ fun discordIconDrawable(
}
return remember(context, bitmap) {
RoundedBitmapDrawableFactory.create(context.resources, bitmap).apply {
isCircular = true
setAntiAlias(true)
}
bitmap.toDrawable(context.resources)
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun customIconDrawable(
foregroundIcon: ByteArray,
backgroundColor: Color = IconReplacement.BlurpleColor,
): Drawable {
val context = LocalContext.current
return remember(context, foregroundIcon) {
val foregroundIconBitmap = BitmapFactory.decodeByteArray(foregroundIcon, 0, foregroundIcon.size)
AdaptiveIconDrawable(
/* backgroundDrawable = */ backgroundColor.toArgb().toDrawable(),
/* foregroundDrawable = */ foregroundIconBitmap.toDrawable(context.resources),
)
}
}
@Composable
fun ColoredDiscordAppIcon(
fun Drawable(
drawable: Drawable,
modifier: Modifier = Modifier,
) {
@ -85,6 +101,9 @@ fun ColoredDiscordAppIcon(
setImageDrawable(drawable)
}
},
update = { imageView ->
imageView.setImageDrawable(drawable)
},
modifier = modifier,
)
}

View file

@ -10,6 +10,8 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@ -90,39 +92,11 @@ fun IconOptionsScreenContent(
.padding(paddingValues)
.padding(horizontal = 20.dp, vertical = 20.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
val color = when (mode) {
IconOptionsMode.Original -> PatchOptions.IconReplacement.BlurpleColor
IconOptionsMode.OldDiscord -> PatchOptions.IconReplacement.OldBlurpleColor
IconOptionsMode.Aliucord -> PatchOptions.IconReplacement.AliucordColor
IconOptionsMode.CustomColor -> selectedColor.toColor()
IconOptionsMode.CustomImage -> Color.Black // TODO: custom image icon preview
}
val throttledColor by throttledState(value = color, throttleMs = 75)
val drawable = discordIconDrawable(
backgroundColor = throttledColor,
oldLogo = mode == IconOptionsMode.OldDiscord,
size = 72.dp,
)
ColoredDiscordAppIcon(
drawable = drawable,
modifier = Modifier.size(24.dp),
)
ColoredDiscordAppIcon(
drawable = drawable,
modifier = Modifier.size(48.dp),
)
ColoredDiscordAppIcon(
drawable = drawable,
modifier = Modifier.size(72.dp),
)
}
IconPreview(
mode = mode,
selectedColor = selectedColor,
selectedImage = selectedImage,
)
HorizontalDivider(Modifier.padding(vertical = 16.dp))
@ -186,6 +160,82 @@ fun IconOptionsScreenContent(
}
}
@Composable
private fun IconPreview(
mode: IconOptionsMode,
selectedColor: HsvColor,
selectedImage: () -> ByteArray?,
) {
Label(
name = stringResource(R.string.iconopts_icon_preview_title),
description = stringResource(R.string.iconopts_icon_preview_desc),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 14.dp),
) {
val drawable = if (mode == IconOptionsMode.CustomImage) {
val image = selectedImage()
if (image != null && Build.VERSION.SDK_INT >= 26) {
customIconDrawable(
foregroundIcon = image,
)
} else {
discordIconDrawable(
backgroundColor = Color.Black,
oldLogo = false,
size = 72.dp,
)
}
} else {
val color = when (mode) {
IconOptionsMode.Original -> PatchOptions.IconReplacement.BlurpleColor
IconOptionsMode.OldDiscord -> PatchOptions.IconReplacement.OldBlurpleColor
IconOptionsMode.Aliucord -> PatchOptions.IconReplacement.AliucordColor
IconOptionsMode.CustomColor -> selectedColor.toColor()
IconOptionsMode.CustomImage -> error("unreachable")
}
val throttledColor by throttledState(value = color, throttleMs = 75)
discordIconDrawable(
backgroundColor = throttledColor,
oldLogo = mode == IconOptionsMode.OldDiscord,
size = 72.dp,
)
}
Drawable(
drawable = drawable,
modifier = Modifier
.size(24.dp)
.clip(CircleShape),
)
Drawable(
drawable = drawable,
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
)
Drawable(
drawable = drawable,
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(4.dp)),
)
Drawable(
drawable = drawable,
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
)
}
}
}
@Composable
private fun CustomColorOptions(
color: HsvColor,

View file

@ -189,8 +189,10 @@
<string name="iconopts_failed_image_too_big">Image size exceeds maximum of 256KiB</string>
<string name="iconopts_btn_open_selection">Select Image</string>
<string name="iconopts_btn_open_example_image">Example image</string>
<string name="iconopts_icon_preview_title">Icon Preview</string>
<string name="iconopts_icon_preview_desc">A preview of the selected options, cropped and scaled to show how the icon will appear on your device</string>
<string name="iconopts_image_preview_title">Preview</string>
<string name="iconopts_image_preview_desc">Uncropped preview of the selected image</string>
<string name="iconopts_image_preview_desc">A full, uncropped preview of the selected image.</string>
<string name="iconopts_image_preview_none">No image selected</string>
<string name="log_title">Log</string>