forked from melod1n/fast-messenger
ui: improve Compose stability and message UI
- Add minute/second abbreviations and kotlin.time-based relative time formatter - Introduce FastPreview and update previews to use AppTheme with dark/dynamic colors - Refactor attachments preview grid & waveform to use ImmutableList and reduce recompositions - Tweak message bubble reply styling and swipe-to-reply animation/haptics - Add Compose Stability Analyzer plugin and enable it in debug builds - Cache shared images by sha256 and improve share intent/chooser text - Minor UX polish (e.g., “No views”) and immutability annotations
This commit is contained in:
@@ -4,6 +4,8 @@ import android.app.Application
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
|
import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
|
||||||
|
import dev.meloda.fast.auth.BuildConfig
|
||||||
import dev.meloda.fast.common.di.applicationModule
|
import dev.meloda.fast.common.di.applicationModule
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
@@ -20,6 +22,8 @@ class AppGlobal : Application(), ImageLoaderFactory {
|
|||||||
AppSettings.init(preferences)
|
AppSettings.init(preferences)
|
||||||
|
|
||||||
initKoin()
|
initKoin()
|
||||||
|
|
||||||
|
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initKoin() {
|
private fun initKoin() {
|
||||||
|
|||||||
@@ -69,9 +69,7 @@ import dev.meloda.fast.ui.theme.LocalNavController
|
|||||||
import dev.meloda.fast.ui.theme.LocalNavRootController
|
import dev.meloda.fast.ui.theme.LocalNavRootController
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.theme.LocalUser
|
import dev.meloda.fast.ui.theme.LocalUser
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
import dev.meloda.fast.ui.util.immutableListOf
|
|
||||||
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
|
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
@@ -309,7 +307,7 @@ fun RootScreen(
|
|||||||
LocalNavController provides navController
|
LocalNavController provides navController
|
||||||
) {
|
) {
|
||||||
var photoViewerInfo by rememberSaveable {
|
var photoViewerInfo by rememberSaveable {
|
||||||
mutableStateOf<Pair<ImmutableList<String>, Int?>?>(null)
|
mutableStateOf<Pair<List<String>, Int?>?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
@@ -333,7 +331,7 @@ fun RootScreen(
|
|||||||
onSettingsButtonClicked = navController::navigateToSettings,
|
onSettingsButtonClicked = navController::navigateToSettings,
|
||||||
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
|
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
|
||||||
onPhotoClicked = { url ->
|
onPhotoClicked = { url ->
|
||||||
photoViewerInfo = immutableListOf(url) to null
|
photoViewerInfo = listOf(url) to null
|
||||||
},
|
},
|
||||||
onMessageClicked = navController::navigateToMessagesHistory,
|
onMessageClicked = navController::navigateToMessagesHistory,
|
||||||
onNavigateToCreateChat = navController::navigateToCreateChat
|
onNavigateToCreateChat = navController::navigateToCreateChat
|
||||||
@@ -344,13 +342,13 @@ fun RootScreen(
|
|||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
onNavigateToChatMaterials = navController::navigateToChatMaterials,
|
onNavigateToChatMaterials = navController::navigateToChatMaterials,
|
||||||
onNavigateToPhotoViewer = { photos, index ->
|
onNavigateToPhotoViewer = { photos, index ->
|
||||||
photoViewerInfo = photos.toImmutableList() to index
|
photoViewerInfo = photos to index
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
chatMaterialsScreen(
|
chatMaterialsScreen(
|
||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
onPhotoClicked = { url ->
|
onPhotoClicked = { url ->
|
||||||
photoViewerInfo = immutableListOf(url) to null
|
photoViewerInfo = listOf(url) to null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
createChatScreen(
|
createChatScreen(
|
||||||
@@ -378,7 +376,9 @@ fun RootScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhotoViewDialog(
|
PhotoViewDialog(
|
||||||
photoViewerInfo = photoViewerInfo,
|
photoViewerInfo = photoViewerInfo?.let { info ->
|
||||||
|
info.first.toImmutableList() to info.second
|
||||||
|
},
|
||||||
onDismiss = { photoViewerInfo = null }
|
onDismiss = { photoViewerInfo = null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
|
|||||||
with(target) {
|
with(target) {
|
||||||
apply(plugin = "com.android.application")
|
apply(plugin = "com.android.application")
|
||||||
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||||
|
apply(plugin = "com.github.skydoves.compose.stability.analyzer")
|
||||||
|
|
||||||
val extension = extensions.getByType<ApplicationExtension>()
|
val extension = extensions.getByType<ApplicationExtension>()
|
||||||
configureAndroidCompose(extension)
|
configureAndroidCompose(extension)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
|
|||||||
with(target) {
|
with(target) {
|
||||||
apply(plugin = "com.android.library")
|
apply(plugin = "com.android.library")
|
||||||
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||||
|
apply(plugin = "com.github.skydoves.compose.stability.analyzer")
|
||||||
|
|
||||||
val extension = extensions.getByType<LibraryExtension>()
|
val extension = extensions.getByType<LibraryExtension>()
|
||||||
extension.androidResources.enable = false
|
extension.androidResources.enable = false
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ plugins {
|
|||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.module.graph) apply true
|
alias(libs.plugins.module.graph) apply true
|
||||||
alias(libs.plugins.versions) apply true
|
alias(libs.plugins.versions) apply true
|
||||||
|
alias(libs.plugins.stability.analyzer) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dev.meloda.fast.common.util
|
package dev.meloda.fast.common.util
|
||||||
|
|
||||||
import com.conena.nanokt.jvm.util.dayOfMonth
|
import com.conena.nanokt.jvm.util.dayOfMonth
|
||||||
import com.conena.nanokt.jvm.util.hour
|
|
||||||
import com.conena.nanokt.jvm.util.hourOfDay
|
import com.conena.nanokt.jvm.util.hourOfDay
|
||||||
import com.conena.nanokt.jvm.util.millisecond
|
import com.conena.nanokt.jvm.util.millisecond
|
||||||
import com.conena.nanokt.jvm.util.minute
|
import com.conena.nanokt.jvm.util.minute
|
||||||
@@ -12,6 +11,12 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
object TimeUtils {
|
object TimeUtils {
|
||||||
|
|
||||||
@@ -56,37 +61,23 @@ object TimeUtils {
|
|||||||
monthShort: () -> String,
|
monthShort: () -> String,
|
||||||
weekShort: () -> String,
|
weekShort: () -> String,
|
||||||
dayShort: () -> String,
|
dayShort: () -> String,
|
||||||
|
minuteShort: () -> String,
|
||||||
|
secondShort: () -> String,
|
||||||
now: () -> String
|
now: () -> String
|
||||||
): String {
|
): String {
|
||||||
val now = Calendar.getInstance()
|
val now = Clock.System.now()
|
||||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
val then = Instant.fromEpochMilliseconds(date)
|
||||||
|
val diff = now - then
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
now.year != then.year -> {
|
diff > 365.days -> "${diff.inWholeDays / 365}${yearShort().lowercase()}"
|
||||||
"${now.year - then.year}${yearShort().lowercase()}"
|
diff > 30.days -> "${diff.inWholeDays / 30}${monthShort().lowercase()}"
|
||||||
}
|
diff > 7.days -> "${diff.inWholeDays / 7}${weekShort().lowercase()}"
|
||||||
|
diff > 1.days -> "${diff.inWholeDays}${dayShort().lowercase()}"
|
||||||
now.month != then.month -> {
|
diff > 1.hours -> "${diff.inWholeHours}h"
|
||||||
"${now.month - then.month}${monthShort().lowercase()}"
|
diff > 1.minutes -> "${diff.inWholeMinutes}${minuteShort().lowercase()}"
|
||||||
}
|
diff > 1.seconds -> "${diff.inWholeSeconds}${secondShort().lowercase()}"
|
||||||
|
else -> now().lowercase()
|
||||||
now.dayOfMonth != then.dayOfMonth -> {
|
|
||||||
val change = now.dayOfMonth - then.dayOfMonth
|
|
||||||
|
|
||||||
if (change % 7 == 0) {
|
|
||||||
"${change / 7}${weekShort().lowercase()}"
|
|
||||||
} else {
|
|
||||||
"$change${dayShort().lowercase()}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
now.hour == then.hour && now.minute == then.minute -> {
|
|
||||||
now().lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
package dev.meloda.fast.common.util
|
package dev.meloda.fast.common.util
|
||||||
|
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
fun String.urlEncode(encoding: String = "utf-8"): String {
|
fun String.urlEncode(encoding: String = "utf-8"): String {
|
||||||
return URLEncoder.encode(this, encoding)
|
return URLEncoder.encode(this, encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.sha256() = this.hashString("SHA-256")
|
||||||
|
|
||||||
|
fun String.hashString(algorithm: String): String {
|
||||||
|
return MessageDigest
|
||||||
|
.getInstance(algorithm)
|
||||||
|
.digest(this.toByteArray())
|
||||||
|
.fold("") { str, it -> str + "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import dev.meloda.fast.model.database.AccountEntity
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class GetCurrentAccountUseCase(
|
class GetCurrentAccountUseCase(private val accountsRepository: AccountsRepository) {
|
||||||
private val accountsRepository: AccountsRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend operator fun invoke(): AccountEntity? = withContext(Dispatchers.IO) {
|
suspend operator fun invoke(): AccountEntity? = withContext(Dispatchers.IO) {
|
||||||
accountsRepository.getAccountById(UserConfig.currentUserId)
|
accountsRepository.getAccountById(UserConfig.currentUserId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ fun VkConvo.asPresentation(
|
|||||||
monthShort = { resources.getString(R.string.month_short) },
|
monthShort = { resources.getString(R.string.month_short) },
|
||||||
weekShort = { resources.getString(R.string.week_short) },
|
weekShort = { resources.getString(R.string.week_short) },
|
||||||
dayShort = { resources.getString(R.string.day_short) },
|
dayShort = { resources.getString(R.string.day_short) },
|
||||||
|
minuteShort = { resources.getString(R.string.minute_short) },
|
||||||
|
secondShort = { resources.getString(R.string.second_short) },
|
||||||
now = { resources.getString(R.string.time_now) },
|
now = { resources.getString(R.string.time_now) },
|
||||||
),
|
),
|
||||||
message = extractMessage(resources, lastMessage, id, peerType),
|
message = extractMessage(resources, lastMessage, id, peerType),
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package dev.meloda.fast.model.api.domain
|
package dev.meloda.fast.model.api.domain
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import dev.meloda.fast.model.api.data.AttachmentType
|
import dev.meloda.fast.model.api.data.AttachmentType
|
||||||
|
|
||||||
|
@Immutable
|
||||||
interface VkAttachment {
|
interface VkAttachment {
|
||||||
val type: AttachmentType
|
val type: AttachmentType
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package dev.meloda.fast.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
||||||
|
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_TYPE_NORMAL
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.Wallpapers.BLUE_DOMINATED_EXAMPLE
|
||||||
|
import androidx.compose.ui.tooling.preview.Wallpapers.GREEN_DOMINATED_EXAMPLE
|
||||||
|
import androidx.compose.ui.tooling.preview.Wallpapers.RED_DOMINATED_EXAMPLE
|
||||||
|
import androidx.compose.ui.tooling.preview.Wallpapers.YELLOW_DOMINATED_EXAMPLE
|
||||||
|
|
||||||
|
@Preview(name = "70%", fontScale = 0.70f)
|
||||||
|
@Preview(name = "85%", fontScale = 0.85f)
|
||||||
|
@Preview(name = "100%", fontScale = 1.0f)
|
||||||
|
@Preview(name = "115%", fontScale = 1.15f)
|
||||||
|
@Preview(name = "130%", fontScale = 1.3f)
|
||||||
|
@Preview(name = "150%", fontScale = 1.5f)
|
||||||
|
@Preview(name = "180%", fontScale = 1.8f)
|
||||||
|
@Preview(name = "200%", fontScale = 2f)
|
||||||
|
|
||||||
|
@Preview(name = "Light")
|
||||||
|
@Preview(name = "Red", wallpaper = RED_DOMINATED_EXAMPLE)
|
||||||
|
@Preview(name = "Blue", wallpaper = BLUE_DOMINATED_EXAMPLE)
|
||||||
|
@Preview(name = "Green", wallpaper = GREEN_DOMINATED_EXAMPLE)
|
||||||
|
@Preview(name = "Yellow", wallpaper = YELLOW_DOMINATED_EXAMPLE)
|
||||||
|
|
||||||
|
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL)
|
||||||
|
@Preview(
|
||||||
|
name = "Dark Red",
|
||||||
|
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||||
|
wallpaper = RED_DOMINATED_EXAMPLE
|
||||||
|
)
|
||||||
|
@Preview(
|
||||||
|
name = "Dark Blue",
|
||||||
|
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||||
|
wallpaper = BLUE_DOMINATED_EXAMPLE
|
||||||
|
)
|
||||||
|
@Preview(
|
||||||
|
name = "Dark Green",
|
||||||
|
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||||
|
wallpaper = GREEN_DOMINATED_EXAMPLE
|
||||||
|
)
|
||||||
|
@Preview(
|
||||||
|
name = "Dark Yellow",
|
||||||
|
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||||
|
wallpaper = YELLOW_DOMINATED_EXAMPLE
|
||||||
|
)
|
||||||
|
|
||||||
|
annotation class FastPreview
|
||||||
@@ -2,6 +2,7 @@ package dev.meloda.fast.ui.components
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
@@ -46,9 +47,14 @@ import androidx.compose.ui.layout.onPlaced
|
|||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewDynamicColors
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewFontScale
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
|
import dev.meloda.fast.ui.common.FastPreview
|
||||||
import dev.meloda.fast.ui.theme.AppTheme
|
import dev.meloda.fast.ui.theme.AppTheme
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
@@ -361,10 +367,10 @@ sealed class SelectionType {
|
|||||||
data object None : SelectionType()
|
data object None : SelectionType()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun MaterialDialogPreview() {
|
private fun MaterialDialogPreview() {
|
||||||
AppTheme {
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
title = "Material Dialog",
|
title = "Material Dialog",
|
||||||
@@ -376,10 +382,10 @@ private fun MaterialDialogPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun MaterialDialogWithListPreview() {
|
private fun MaterialDialogWithListPreview() {
|
||||||
AppTheme {
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
title = "Material Dialog",
|
title = "Material Dialog",
|
||||||
@@ -393,10 +399,10 @@ private fun MaterialDialogWithListPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun MaterialDialogWithCustomContent() {
|
private fun MaterialDialogWithCustomContent() {
|
||||||
AppTheme {
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
title = "Material Dialog",
|
title = "Material Dialog",
|
||||||
@@ -425,10 +431,10 @@ private fun MaterialDialogWithCustomContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun MaterialDialogWithOnlyCustomContent() {
|
private fun MaterialDialogWithOnlyCustomContent() {
|
||||||
AppTheme {
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
MaterialDialog(onDismissRequest = {}) {
|
MaterialDialog(onDismissRequest = {}) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.meloda.fast.ui.components
|
package dev.meloda.fast.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -8,15 +9,17 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.dp
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
|
import dev.meloda.fast.ui.common.FastPreview
|
||||||
|
import dev.meloda.fast.ui.theme.AppTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoItemsView(
|
fun NoItemsView(
|
||||||
@@ -49,11 +52,15 @@ fun NoItemsView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun NoItemsViewPreview() {
|
private fun NoItemsViewPreview() {
|
||||||
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
|
Surface {
|
||||||
NoItemsView(
|
NoItemsView(
|
||||||
customText = "Nothing here...",
|
customText = "Nothing here...",
|
||||||
buttonText = "Refresh"
|
buttonText = "Refresh"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
import org.koin.core.parameter.ParametersDefinition
|
import org.koin.core.parameter.ParametersDefinition
|
||||||
import org.koin.core.qualifier.Qualifier
|
import org.koin.core.qualifier.Qualifier
|
||||||
|
|
||||||
|
@Suppress("ParamsComparedByRef")
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(
|
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
|||||||
|
|
||||||
operator fun get(index: Int): T = values[index]
|
operator fun get(index: Int): T = values[index]
|
||||||
|
|
||||||
inline fun forEach(action: (T) -> Unit) {
|
|
||||||
for (element in values) action(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <R> map(transform: (T) -> R): ImmutableList<R> {
|
inline fun <R> map(transform: (T) -> R): ImmutableList<R> {
|
||||||
return values.map(transform).toImmutableList()
|
return values.map(transform).toImmutableList()
|
||||||
}
|
}
|
||||||
@@ -49,6 +45,8 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
|||||||
if (elements.isNotEmpty()) copyOf(elements.asList()) else empty()
|
if (elements.isNotEmpty()) copyOf(elements.asList()) else empty()
|
||||||
|
|
||||||
fun <T> of(element: T) = ImmutableList(listOf(element))
|
fun <T> of(element: T) = ImmutableList(listOf(element))
|
||||||
|
|
||||||
|
fun <T> ImmutableList<T>?.orEmpty(): ImmutableList<T> = this ?: emptyImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iterator(): Iterator<T> = values.listIterator()
|
override fun iterator(): Iterator<T> = values.listIterator()
|
||||||
@@ -59,5 +57,3 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
|||||||
fun <T> emptyImmutableList(): ImmutableList<T> = ImmutableList(emptyList())
|
fun <T> emptyImmutableList(): ImmutableList<T> = ImmutableList(emptyList())
|
||||||
|
|
||||||
fun <T> immutableListOf(vararg elements: T) = ImmutableList(listOf(elements = elements))
|
fun <T> immutableListOf(vararg elements: T) = ImmutableList(listOf(elements = elements))
|
||||||
|
|
||||||
fun <T> ImmutableList<T>?.orEmpty(): ImmutableList<T> = this ?: emptyImmutableList()
|
|
||||||
|
|||||||
@@ -232,8 +232,10 @@
|
|||||||
<string name="month_short">М</string>
|
<string name="month_short">М</string>
|
||||||
<string name="week_short">Н</string>
|
<string name="week_short">Н</string>
|
||||||
<string name="day_short">Д</string>
|
<string name="day_short">Д</string>
|
||||||
|
<string name="second_short">С</string>
|
||||||
<string name="time_now">Сейчас</string>
|
<string name="time_now">Сейчас</string>
|
||||||
|
|
||||||
<string name="confirm_chat_create_with_title">Вы действительно хотите создать чат «%s»?</string>
|
<string name="confirm_chat_create_with_title">Вы действительно хотите создать чат «%s»?</string>
|
||||||
<string name="confirm_chat_create_empty_with_title">Вы действительно хотите создать чат «%s» только с собой?</string>
|
<string name="confirm_chat_create_empty_with_title">Вы действительно хотите создать чат «%s» только с собой?</string>
|
||||||
|
<string name="minute_short">М</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
<string name="app_name" translatable="false">Fast</string>
|
<string name="app_name" translatable="false">Fast</string>
|
||||||
<string name="fast_messenger" translatable="false">Fast Messenger</string>
|
<string name="fast_messenger" translatable="false">Fast Messenger</string>
|
||||||
|
|
||||||
@@ -297,6 +297,8 @@
|
|||||||
<string name="month_short">M</string>
|
<string name="month_short">M</string>
|
||||||
<string name="week_short">W</string>
|
<string name="week_short">W</string>
|
||||||
<string name="day_short">D</string>
|
<string name="day_short">D</string>
|
||||||
|
<string name="minute_short">M</string>
|
||||||
|
<string name="second_short">S</string>
|
||||||
<string name="time_now">Now</string>
|
<string name="time_now">Now</string>
|
||||||
|
|
||||||
<string name="confirm_chat_create_with_title">Are you sure you want to create chat «%s»?</string>
|
<string name="confirm_chat_create_with_title">Are you sure you want to create chat «%s»?</string>
|
||||||
|
|||||||
+6
-2
@@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -44,7 +45,6 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||||
@@ -53,9 +53,11 @@ import dev.meloda.fast.auth.captcha.CaptchaViewModel
|
|||||||
import dev.meloda.fast.auth.captcha.CaptchaViewModelImpl
|
import dev.meloda.fast.auth.captcha.CaptchaViewModelImpl
|
||||||
import dev.meloda.fast.auth.captcha.model.CaptchaScreenState
|
import dev.meloda.fast.auth.captcha.model.CaptchaScreenState
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
|
import dev.meloda.fast.ui.common.FastPreview
|
||||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||||
|
import dev.meloda.fast.ui.theme.AppTheme
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -258,12 +260,14 @@ fun CaptchaScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CaptchaScreenPreview() {
|
private fun CaptchaScreenPreview() {
|
||||||
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
CaptchaScreen(
|
CaptchaScreen(
|
||||||
screenState = CaptchaScreenState.EMPTY.copy(
|
screenState = CaptchaScreenState.EMPTY.copy(
|
||||||
code = "zcuecz"
|
code = "zcuecz"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package dev.meloda.fast.auth.login.navigation
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
|||||||
+6
-1
@@ -3,6 +3,7 @@ package dev.meloda.fast.auth.validation.presentation
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.shrinkHorizontally
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -54,9 +55,11 @@ import dev.meloda.fast.auth.validation.ValidationViewModelImpl
|
|||||||
import dev.meloda.fast.auth.validation.model.ValidationScreenState
|
import dev.meloda.fast.auth.validation.model.ValidationScreenState
|
||||||
import dev.meloda.fast.auth.validation.model.ValidationType
|
import dev.meloda.fast.auth.validation.model.ValidationType
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
|
import dev.meloda.fast.ui.common.FastPreview
|
||||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||||
|
import dev.meloda.fast.ui.theme.AppTheme
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -301,9 +304,10 @@ fun ValidationScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ValidationScreenPreview() {
|
private fun ValidationScreenPreview() {
|
||||||
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
ValidationScreen(
|
ValidationScreen(
|
||||||
screenState = ValidationScreenState.EMPTY.copy(
|
screenState = ValidationScreenState.EMPTY.copy(
|
||||||
phoneMask = "+7 (***) ***-**-21",
|
phoneMask = "+7 (***) ***-**-21",
|
||||||
@@ -311,4 +315,5 @@ private fun ValidationScreenPreview() {
|
|||||||
),
|
),
|
||||||
validationType = ValidationType.SMS
|
validationType = ValidationType.SMS
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import androidx.compose.ui.unit.DpOffset
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.skydoves.compose.stability.runtime.TraceRecomposition
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
|||||||
+40
-24
@@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory.presentation
|
|||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
@@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
@@ -30,7 +32,6 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.domain.util.annotated
|
import dev.meloda.fast.domain.util.annotated
|
||||||
import dev.meloda.fast.messageshistory.presentation.attachments.Attachments
|
import dev.meloda.fast.messageshistory.presentation.attachments.Attachments
|
||||||
@@ -38,14 +39,12 @@ import dev.meloda.fast.messageshistory.presentation.attachments.Reply
|
|||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkStickerDomain
|
import dev.meloda.fast.model.api.domain.VkStickerDomain
|
||||||
import dev.meloda.fast.model.api.domain.VkVideoMessageDomain
|
import dev.meloda.fast.model.api.domain.VkVideoMessageDomain
|
||||||
|
import dev.meloda.fast.ui.common.FastPreview
|
||||||
import dev.meloda.fast.ui.model.vk.SendingStatus
|
import dev.meloda.fast.ui.model.vk.SendingStatus
|
||||||
import dev.meloda.fast.ui.theme.AppTheme
|
import dev.meloda.fast.ui.theme.AppTheme
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import dev.meloda.fast.ui.util.darken
|
|
||||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||||
import dev.meloda.fast.ui.util.isDark
|
|
||||||
import dev.meloda.fast.ui.util.lighten
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBubble(
|
fun MessageBubble(
|
||||||
@@ -120,20 +119,22 @@ fun MessageBubble(
|
|||||||
if (replyTitle != null) {
|
if (replyTitle != null) {
|
||||||
Reply(
|
Reply(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(if (attachments == null || text != null) 0.dp else 4.dp)
|
.padding(if (attachments == null || text != null) 0.dp else 0.dp)
|
||||||
.width(with(density) { containerWidth.toDp() }),
|
.width(with(density) { containerWidth.toDp() }),
|
||||||
bottomPadding = if (attachments == null || text != null) 0.dp else 4.dp,
|
bottomPadding = if (attachments == null || text != null) 0.dp else 4.dp,
|
||||||
shape = RoundedCornerShape(
|
shape = RoundedCornerShape(
|
||||||
topStart = 16.dp,
|
topStart = 16.dp,
|
||||||
topEnd = 16.dp,
|
topEnd = 16.dp,
|
||||||
bottomStart = if (attachments == null || text != null) 0.dp else 16.dp,
|
bottomStart = if (attachments == null || text != null) 0.dp else 0.dp,
|
||||||
bottomEnd = if (attachments == null || text != null) 0.dp else 16.dp
|
bottomEnd = if (attachments == null || text != null) 0.dp else 0.dp
|
||||||
),
|
),
|
||||||
onClick = onReplyClick,
|
onClick = onReplyClick,
|
||||||
title = replyTitle,
|
title = replyTitle,
|
||||||
summary = replySummary,
|
summary = replySummary,
|
||||||
backgroundColor = colors.container,
|
backgroundColor = colors.replyContainer,
|
||||||
innerBackgroundColor = colors.replyContainer
|
innerBackgroundColor = colors.replyInnerContainer,
|
||||||
|
titleColor = colors.replyTitle,
|
||||||
|
textColor = colors.replyText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ fun MessageBubble(
|
|||||||
.padding(
|
.padding(
|
||||||
start = 8.dp,
|
start = 8.dp,
|
||||||
end = 8.dp,
|
end = 8.dp,
|
||||||
top = if (replyTitle != null) 0.dp else 6.dp,
|
top = if (replyTitle != null) 4.dp else 6.dp,
|
||||||
bottom = if (replyTitle != null) 4.dp else 6.dp
|
bottom = if (replyTitle != null) 4.dp else 6.dp
|
||||||
)
|
)
|
||||||
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
||||||
@@ -211,17 +212,25 @@ fun MessageBubble(
|
|||||||
attachmentsContainerWidth = it.size.width
|
attachmentsContainerWidth = it.size.width
|
||||||
}
|
}
|
||||||
.clip(
|
.clip(
|
||||||
if (!shouldShowBubble) RoundedCornerShape(24.dp)
|
if (!shouldShowBubble) {
|
||||||
else RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
|
topStart = if (replyTitle != null) 0.dp else 24.dp,
|
||||||
|
topEnd = if (replyTitle != null) 0.dp else 24.dp,
|
||||||
bottomEnd = 24.dp,
|
bottomEnd = 24.dp,
|
||||||
bottomStart = 24.dp,
|
bottomStart = 24.dp,
|
||||||
|
)
|
||||||
|
} else RoundedCornerShape(
|
||||||
topStart = 0.dp,
|
topStart = 0.dp,
|
||||||
topEnd = 0.dp
|
topEnd = 0.dp,
|
||||||
|
bottomEnd = 24.dp,
|
||||||
|
bottomStart = 24.dp,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.background(attachmentBackgroundColor)
|
.background(attachmentBackgroundColor)
|
||||||
) {
|
) {
|
||||||
Attachments(
|
Attachments(
|
||||||
|
withText = text != null,
|
||||||
|
withReply = replyTitle != null,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
onClick = currentOnClick,
|
onClick = currentOnClick,
|
||||||
@@ -261,6 +270,9 @@ private data class MessageBubbleColors(
|
|||||||
val container: Color,
|
val container: Color,
|
||||||
val content: Color,
|
val content: Color,
|
||||||
val replyContainer: Color,
|
val replyContainer: Color,
|
||||||
|
val replyInnerContainer: Color,
|
||||||
|
val replyTitle: Color,
|
||||||
|
val replyText: Color
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -268,31 +280,35 @@ private fun messageBubbleColors(isOut: Boolean): MessageBubbleColors {
|
|||||||
return if (isOut) {
|
return if (isOut) {
|
||||||
val containerColor = MaterialTheme.colorScheme.primaryContainer
|
val containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
|
||||||
val replyContainerColor = if (containerColor.isDark()) {
|
|
||||||
containerColor.lighten(0.15f)
|
|
||||||
} else {
|
|
||||||
containerColor.darken(0.075f)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageBubbleColors(
|
MessageBubbleColors(
|
||||||
container = containerColor,
|
container = containerColor,
|
||||||
content = MaterialTheme.colorScheme.onPrimaryContainer,
|
content = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
replyContainer = replyContainerColor
|
replyContainer = containerColor,
|
||||||
|
replyInnerContainer = MaterialTheme.colorScheme.background.copy(
|
||||||
|
if (isSystemInDarkTheme()) 0.3f else 0.45f
|
||||||
|
),
|
||||||
|
replyTitle = MaterialTheme.colorScheme.primary,
|
||||||
|
replyText = MaterialTheme.colorScheme.onBackground
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
val containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
|
||||||
MessageBubbleColors(
|
MessageBubbleColors(
|
||||||
container = MaterialTheme.colorScheme.surfaceContainer,
|
container = containerColor,
|
||||||
content = MaterialTheme.colorScheme.onSurface,
|
content = MaterialTheme.colorScheme.onSurface,
|
||||||
replyContainer = MaterialTheme.colorScheme.surfaceContainerHighest
|
replyContainer = containerColor,
|
||||||
|
replyInnerContainer = MaterialTheme.colorScheme.surfaceColorAtElevation(20.dp),
|
||||||
|
replyTitle = MaterialTheme.colorScheme.primary,
|
||||||
|
replyText = MaterialTheme.colorScheme.onBackground
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun Bubble() {
|
private fun Bubble() {
|
||||||
AppTheme(
|
AppTheme(
|
||||||
useDarkTheme = true,
|
useDarkTheme = isSystemInDarkTheme(),
|
||||||
useDynamicColors = true
|
useDynamicColors = true
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
|||||||
+3
-1
@@ -188,7 +188,9 @@ fun MessageOptionsDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MessageOptionItem(
|
MessageOptionItem(
|
||||||
title = viewCount?.let { "$it views" } ?: "...",
|
title = viewCount?.let {
|
||||||
|
if (it == 0) "No views" else "$it views"
|
||||||
|
} ?: "...",
|
||||||
iconResId = R.drawable.ic_visibility_round_24,
|
iconResId = R.drawable.ic_visibility_round_24,
|
||||||
tintColor = primaryColor,
|
tintColor = primaryColor,
|
||||||
onClick = {}
|
onClick = {}
|
||||||
|
|||||||
+33
-17
@@ -26,9 +26,12 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -40,7 +43,6 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
@@ -193,11 +195,22 @@ fun MessagesList(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val offsetX = remember { Animatable(0f) }
|
var animate by remember { mutableStateOf(false) }
|
||||||
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var offsetDistinct by remember { mutableFloatStateOf(0f) }
|
||||||
|
val offsetAnimatable = remember { Animatable(0f) }
|
||||||
|
|
||||||
val offsetDistinct by snapshotFlow { offsetX.value }
|
LaunchedEffect(offsetX) {
|
||||||
|
if (!animate) {
|
||||||
|
offsetAnimatable.snapTo(offsetX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { offsetX.minus(5f).coerceIn(-100f, 0f) }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collectAsStateWithLifecycle(offsetX)
|
.collect { offsetDistinct = it }
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(offsetDistinct) {
|
LaunchedEffect(offsetDistinct) {
|
||||||
if (offsetDistinct == -100f && AppSettings.General.enableHaptic) {
|
if (offsetDistinct == -100f && AppSettings.General.enableHaptic) {
|
||||||
@@ -222,32 +235,35 @@ fun MessagesList(
|
|||||||
},
|
},
|
||||||
onClick = { onMessageClicked(item.id) }
|
onClick = { onMessageClicked(item.id) }
|
||||||
)
|
)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(item.cmId) {
|
||||||
detectHorizontalDragGestures(
|
detectHorizontalDragGestures(
|
||||||
onDragCancel = {
|
onDragCancel = {
|
||||||
if (offsetX.value == -100f) {
|
if (offsetX == -100f) {
|
||||||
onRequestMessageReply(item.cmId)
|
onRequestMessageReply(item.cmId)
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
offsetX.animateTo(0f)
|
animate = true
|
||||||
|
offsetX = 0f
|
||||||
|
offsetAnimatable.animateTo(0f)
|
||||||
|
animate = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
if (offsetX.value == -100f) {
|
if (offsetX == -100f) {
|
||||||
onRequestMessageReply(item.cmId)
|
onRequestMessageReply(item.cmId)
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
offsetX.animateTo(0f)
|
animate = true
|
||||||
|
offsetX = 0f
|
||||||
|
offsetAnimatable.animateTo(0f)
|
||||||
|
animate = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHorizontalDrag = { _, dragAmount ->
|
onHorizontalDrag = { change, dragAmount ->
|
||||||
scope.launch {
|
change.consume()
|
||||||
offsetX.snapTo(
|
offsetX = (offsetX + dragAmount).coerceIn(-100f, 0f)
|
||||||
(offsetX.value + dragAmount).coerceIn(-100f, 0f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -278,7 +294,7 @@ fun MessagesList(
|
|||||||
onRequestScrollToCmId(item.replyCmId!!)
|
onRequestScrollToCmId(item.replyCmId!!)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
offsetX = offsetX.value
|
offsetX = offsetAnimatable.value
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
IncomingMessageBubble(
|
IncomingMessageBubble(
|
||||||
@@ -305,7 +321,7 @@ fun MessagesList(
|
|||||||
onRequestScrollToCmId(item.replyCmId!!)
|
onRequestScrollToCmId(item.replyCmId!!)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
offsetX = offsetX.value
|
offsetX = offsetAnimatable.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-12
@@ -16,7 +16,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -53,6 +52,8 @@ private val previewTypes = listOf(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Attachments(
|
fun Attachments(
|
||||||
|
withText: Boolean,
|
||||||
|
withReply: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
attachments: ImmutableList<out VkAttachment>,
|
attachments: ImmutableList<out VkAttachment>,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
onClick: (VkAttachment) -> Unit = {},
|
||||||
@@ -64,23 +65,20 @@ fun Attachments(
|
|||||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
val previewAttachments by remember(attachments) {
|
val previewAttachments = remember(attachments) {
|
||||||
derivedStateOf {
|
|
||||||
attachments.values.filter { it.type in previewTypes }
|
attachments.values.filter { it.type in previewTypes }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val nonPreviewAttachments by remember(attachments) {
|
val nonPreviewAttachments = remember(attachments) {
|
||||||
derivedStateOf {
|
attachments.values.filterNot { it.type in previewTypes }.sortedBy { it.type.ordinal }
|
||||||
attachments.values.filterNot { it.type in previewTypes }
|
|
||||||
.sortedBy { it.type.ordinal }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previewAttachments.isNotEmpty()) {
|
if (previewAttachments.isNotEmpty()) {
|
||||||
Previews(
|
DynamicPreviewGrid(
|
||||||
|
withText = withText,
|
||||||
|
withReply = withReply,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
photos = previewAttachments
|
previews = previewAttachments
|
||||||
.map(VkAttachment::asUiPhoto)
|
.map(VkAttachment::asUiPhoto)
|
||||||
.toImmutableList(),
|
.toImmutableList(),
|
||||||
onClick = { index ->
|
onClick = { index ->
|
||||||
@@ -187,7 +185,8 @@ fun Attachments(
|
|||||||
.let(::downsampleWaveform)
|
.let(::downsampleWaveform)
|
||||||
.let(::downsampleWaveform)
|
.let(::downsampleWaveform)
|
||||||
.let { amplifyWaveform(it, audioMessage.waveform.max()) }
|
.let { amplifyWaveform(it, audioMessage.waveform.max()) }
|
||||||
.map(::WaveForm),
|
.map(::WaveForm)
|
||||||
|
.toImmutableList(),
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
onPlayClick = {}
|
onPlayClick = {}
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-1
@@ -26,11 +26,12 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.times
|
import androidx.compose.ui.unit.times
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.FastIconButton
|
import dev.meloda.fast.ui.components.FastIconButton
|
||||||
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import kotlin.collections.forEachIndexed
|
import kotlin.collections.forEachIndexed
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioMessage(
|
fun AudioMessage(
|
||||||
waveform: List<WaveForm>,
|
waveform: ImmutableList<WaveForm>,
|
||||||
isPlaying: Boolean,
|
isPlaying: Boolean,
|
||||||
onPlayClick: () -> Unit,
|
onPlayClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|||||||
+29
-25
@@ -1,6 +1,6 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
package dev.meloda.fast.messageshistory.presentation.attachments
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -33,25 +33,12 @@ import dev.meloda.fast.ui.components.FastIconButton
|
|||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Previews(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
photos: ImmutableList<UiPreview>,
|
|
||||||
onClick: (index: Int) -> Unit = {},
|
|
||||||
onLongClick: (index: Int) -> Unit = {}
|
|
||||||
) {
|
|
||||||
DynamicPreviewGrid(
|
|
||||||
modifier = modifier,
|
|
||||||
photos = photos,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DynamicPreviewGrid(
|
fun DynamicPreviewGrid(
|
||||||
photos: ImmutableList<UiPreview>,
|
withText: Boolean,
|
||||||
|
withReply: Boolean,
|
||||||
|
previews: ImmutableList<UiPreview>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: (index: Int) -> Unit = {},
|
onClick: (index: Int) -> Unit = {},
|
||||||
onLongClick: (index: Int) -> Unit = {}
|
onLongClick: (index: Int) -> Unit = {}
|
||||||
@@ -60,16 +47,27 @@ fun DynamicPreviewGrid(
|
|||||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||||
|
|
||||||
val spacing = 2.dp
|
val spacing = 2.dp
|
||||||
val shape = RoundedCornerShape(8.dp)
|
val cornerRadius = 20.dp
|
||||||
|
val insideRadius = 4.dp
|
||||||
|
|
||||||
BoxWithConstraints(modifier = modifier) {
|
val calculateShape by rememberUpdatedState { outer: Int, inner: Int, outLast: Int, inLast: Int ->
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = if (!withText && !withReply && outer == 0 && inner == 0) cornerRadius else insideRadius,
|
||||||
|
topEnd = if (!withText && !withReply && outer == 0 && inner == inLast) cornerRadius else insideRadius,
|
||||||
|
bottomStart = if (outer == outLast && inner == 0) cornerRadius else insideRadius,
|
||||||
|
bottomEnd = if (outer == outLast && inner == inLast) cornerRadius else insideRadius
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxWithConstraints(modifier = modifier.padding(4.dp)) {
|
||||||
val maxWidthPx = with(LocalDensity.current) { maxWidth.toPx() }
|
val maxWidthPx = with(LocalDensity.current) { maxWidth.toPx() }
|
||||||
val spacingPx = with(LocalDensity.current) { spacing.toPx() }
|
val spacingPx = with(LocalDensity.current) { spacing.toPx() }
|
||||||
|
|
||||||
val rows = photos.chunked(3)
|
val rows = previews.chunked(3)
|
||||||
|
Log.d("ROWS", "DynamicPreviewGrid: ${rows.size}")
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
|
Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
|
||||||
rows.forEachIndexed { index, row ->
|
rows.forEachIndexed { outerIndex, row ->
|
||||||
val aspectRatios = row.map { it.width.toFloat() / it.height }
|
val aspectRatios = row.map { it.width.toFloat() / it.height }
|
||||||
val totalAspect = aspectRatios.sum()
|
val totalAspect = aspectRatios.sum()
|
||||||
|
|
||||||
@@ -80,6 +78,8 @@ fun DynamicPreviewGrid(
|
|||||||
val height = photoWidthPx / aspectRatios[index]
|
val height = photoWidthPx / aspectRatios[index]
|
||||||
val heightDp = with(LocalDensity.current) { height.toDp() }
|
val heightDp = with(LocalDensity.current) { height.toDp() }
|
||||||
|
|
||||||
|
val shape = calculateShape(outerIndex, index, rows.lastIndex, row.lastIndex)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(heightDp)
|
.height(heightDp)
|
||||||
@@ -95,14 +95,14 @@ fun DynamicPreviewGrid(
|
|||||||
.height(heightDp)
|
.height(heightDp)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = { currentOnLongClick(index) },
|
onLongClick = { currentOnLongClick(outerIndex * 3 + index) },
|
||||||
onClick = { currentOnClick(index) }
|
onClick = { currentOnClick(outerIndex * 3 + index) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (preview.isVideo) {
|
if (preview.isVideo) {
|
||||||
FastIconButton(
|
FastIconButton(
|
||||||
onClick = { currentOnClick(index) },
|
onClick = { currentOnClick(outerIndex * 3 + index) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp)
|
.size(36.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
@@ -146,6 +146,10 @@ fun PreviewDynamicPhotoGrid() {
|
|||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
DynamicPreviewGrid(photos = mockPhotos.take(10).toImmutableList())
|
DynamicPreviewGrid(
|
||||||
|
withText = false,
|
||||||
|
withReply = false,
|
||||||
|
previews = mockPhotos.take(10).toImmutableList()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-15
@@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory.presentation.attachments
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -23,12 +24,15 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import dev.meloda.fast.domain.util.annotated
|
import dev.meloda.fast.domain.util.annotated
|
||||||
import dev.meloda.fast.domain.util.orEmpty
|
import dev.meloda.fast.domain.util.orEmpty
|
||||||
|
import dev.meloda.fast.ui.common.FastPreview
|
||||||
|
import dev.meloda.fast.ui.theme.AppTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Reply(
|
fun Reply(
|
||||||
@@ -37,6 +41,8 @@ fun Reply(
|
|||||||
shape: Shape,
|
shape: Shape,
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
innerBackgroundColor: Color,
|
innerBackgroundColor: Color,
|
||||||
|
titleColor: Color,
|
||||||
|
textColor: Color,
|
||||||
title: String,
|
title: String,
|
||||||
summary: AnnotatedString?,
|
summary: AnnotatedString?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -47,7 +53,7 @@ fun Reply(
|
|||||||
color = backgroundColor,
|
color = backgroundColor,
|
||||||
shape = shape
|
shape = shape
|
||||||
)
|
)
|
||||||
.height(40.dp)
|
.height(48.dp)
|
||||||
.padding(
|
.padding(
|
||||||
top = 4.dp,
|
top = 4.dp,
|
||||||
start = 4.dp,
|
start = 4.dp,
|
||||||
@@ -66,7 +72,7 @@ fun Reply(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(3.dp)
|
.width(3.dp)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.background(MaterialTheme.colorScheme.onBackground)
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
@@ -77,17 +83,22 @@ fun Reply(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
color = titleColor
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(summary != null) {
|
AnimatedVisibility(summary != null) {
|
||||||
Text(
|
Text(
|
||||||
text = summary.orEmpty(),
|
text = summary.orEmpty(),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
color = textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +109,9 @@ fun Reply(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ReplyBasePreview(
|
private fun ReplyBasePreview(
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
innerBackgroundColor: Color
|
innerBackgroundColor: Color,
|
||||||
|
titleColor: Color,
|
||||||
|
textColor: Color
|
||||||
) {
|
) {
|
||||||
Reply(
|
Reply(
|
||||||
modifier = Modifier.width(120.dp),
|
modifier = Modifier.width(120.dp),
|
||||||
@@ -111,24 +124,42 @@ private fun ReplyBasePreview(
|
|||||||
summary = "2 photos".annotated(),
|
summary = "2 photos".annotated(),
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
innerBackgroundColor = innerBackgroundColor,
|
innerBackgroundColor = innerBackgroundColor,
|
||||||
bottomPadding = 0.dp
|
titleColor = titleColor,
|
||||||
|
textColor = textColor,
|
||||||
|
bottomPadding = 0.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun IncomingReplyPreview() {
|
private fun IncomingReplyPreview() {
|
||||||
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
ReplyBasePreview(
|
ReplyBasePreview(
|
||||||
backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||||
innerBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(20.dp)
|
innerBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(20.dp),
|
||||||
|
titleColor = MaterialTheme.colorScheme.primary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onBackground
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@FastPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun OutgoingReplyPreview() {
|
private fun OutgoingReplyPreview() {
|
||||||
ReplyBasePreview(
|
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||||
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
val bg = MaterialTheme.colorScheme.primaryContainer
|
||||||
innerBackgroundColor = MaterialTheme.colorScheme.inversePrimary
|
val inner = MaterialTheme.colorScheme.background.copy(
|
||||||
|
if (isSystemInDarkTheme()) 0.3f else 0.6f
|
||||||
)
|
)
|
||||||
|
val title = MaterialTheme.colorScheme.primary
|
||||||
|
val text = MaterialTheme.colorScheme.onBackground
|
||||||
|
|
||||||
|
|
||||||
|
ReplyBasePreview(
|
||||||
|
backgroundColor = bg,
|
||||||
|
innerBackgroundColor = inner,
|
||||||
|
titleColor = title,
|
||||||
|
textColor = text
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -5,7 +5,6 @@ import android.content.ClipboardManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||||
@@ -17,9 +16,11 @@ import coil.imageLoader
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.common.model.UiImage
|
import dev.meloda.fast.common.model.UiImage
|
||||||
|
import dev.meloda.fast.common.util.sha256
|
||||||
import dev.meloda.fast.photoviewer.model.PhotoViewArguments
|
import dev.meloda.fast.photoviewer.model.PhotoViewArguments
|
||||||
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
|
||||||
import dev.meloda.fast.photoviewer.navigation.PhotoView
|
import dev.meloda.fast.photoviewer.navigation.PhotoView
|
||||||
|
import dev.meloda.fast.ui.R
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -28,9 +29,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
|
|
||||||
interface PhotoViewViewModel {
|
interface PhotoViewViewModel {
|
||||||
val screenState: StateFlow<PhotoViewScreenState>
|
val screenState: StateFlow<PhotoViewScreenState>
|
||||||
@@ -99,9 +97,10 @@ class PhotoViewViewModelImpl(
|
|||||||
type = "image/png"
|
type = "image/png"
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
clipData = ClipData.newRawUri(null, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
val chooserIntent = Intent.createChooser(intent, null)
|
val chooserIntent = Intent.createChooser(intent, "Share image via...")
|
||||||
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
chooserIntent
|
chooserIntent
|
||||||
}
|
}
|
||||||
@@ -186,6 +185,13 @@ class PhotoViewViewModelImpl(
|
|||||||
|
|
||||||
private suspend fun downloadAndStoreImageToCache(url: String): File? =
|
private suspend fun downloadAndStoreImageToCache(url: String): File? =
|
||||||
runCatching {
|
runCatching {
|
||||||
|
val imagesDir = File(applicationContext.cacheDir, "images")
|
||||||
|
if (!imagesDir.exists()) {
|
||||||
|
imagesDir.mkdirs()
|
||||||
|
}
|
||||||
|
val imageFile = File(imagesDir, "${url.sha256()}.png")
|
||||||
|
if (imageFile.exists()) return imageFile
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
screenState.setValue { old -> old.copy(isLoading = true) }
|
screenState.setValue { old -> old.copy(isLoading = true) }
|
||||||
|
|
||||||
@@ -198,9 +204,6 @@ class PhotoViewViewModelImpl(
|
|||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
val imagesDir = File(applicationContext.cacheDir, "images")
|
|
||||||
if (!imagesDir.exists()) imagesDir.mkdirs()
|
|
||||||
val imageFile = File(imagesDir, "shared_image_id${UUID.randomUUID()}.png")
|
|
||||||
FileOutputStream(imageFile).use {
|
FileOutputStream(imageFile).use {
|
||||||
drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it)
|
drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ agp = "9.0.0"
|
|||||||
retrofit = "3.0.0"
|
retrofit = "3.0.0"
|
||||||
eithernet = "2.0.0"
|
eithernet = "2.0.0"
|
||||||
haze = "1.7.1"
|
haze = "1.7.1"
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.3.10"
|
||||||
ksp = "2.3.4"
|
ksp = "2.3.4"
|
||||||
moduleGraph = "2.9.0"
|
moduleGraph = "2.9.0"
|
||||||
versions = "0.53.0"
|
versions = "0.53.0"
|
||||||
|
stability-analyzer = "0.6.6"
|
||||||
|
|
||||||
compose-bom = "2026.01.00"
|
compose-bom = "2026.01.01"
|
||||||
koin = "4.1.1"
|
koin = "4.1.1"
|
||||||
|
|
||||||
accompanist = "0.37.3"
|
accompanist = "0.37.3"
|
||||||
@@ -117,6 +118,7 @@ room = { id = "androidx.room", version.ref = "room" }
|
|||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" }
|
module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" }
|
||||||
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
|
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
|
||||||
|
stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version.ref = "stability-analyzer" }
|
||||||
|
|
||||||
#project plugins
|
#project plugins
|
||||||
fast-android-application = { id = "fast.android.application", version = "unspecified" }
|
fast-android-application = { id = "fast.android.application", version = "unspecified" }
|
||||||
|
|||||||
Reference in New Issue
Block a user