forked from melod1n/fast-messenger
Refactor: Introduce FullScreenContainedLoader and use rememberUpdatedState
This commit introduces `FullScreenContainedLoader` and replaces usages of `FullScreenLoader` where appropriate. It also updates several composables to use `rememberUpdatedState` for lambda parameters to ensure the latest versions are used. Additionally, the following changes are included: - Add a setting to show/hide the attachment button in the chat input bar. - Implement navigation to `PhotoViewScreen` when a photo attachment is clicked in a message. - Add "Copy link" and "Copy image" actions to `PhotoViewScreen`. - Remove unused settings and their corresponding logic from `SettingsViewModel` and `UserSettings`.
This commit is contained in:
@@ -144,7 +144,8 @@ fun RootScreen(
|
|||||||
messagesHistoryScreen(
|
messagesHistoryScreen(
|
||||||
onError = viewModel::onError,
|
onError = viewModel::onError,
|
||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
onNavigateToChatMaterials = navController::navigateToChatMaterials
|
onNavigateToChatMaterials = navController::navigateToChatMaterials,
|
||||||
|
onNavigateToPhotoViewer = navController::navigateToPhotoView
|
||||||
)
|
)
|
||||||
chatMaterialsScreen(
|
chatMaterialsScreen(
|
||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ object AppSettings {
|
|||||||
)
|
)
|
||||||
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
|
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
|
||||||
|
|
||||||
|
var showAttachmentButton: Boolean
|
||||||
|
get() = get(
|
||||||
|
SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON
|
||||||
|
)
|
||||||
|
set(value) = put(SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, value)
|
||||||
|
|
||||||
var enableHaptic: Boolean
|
var enableHaptic: Boolean
|
||||||
get() = get(
|
get() = get(
|
||||||
SettingsKeys.KEY_ENABLE_HAPTIC,
|
SettingsKeys.KEY_ENABLE_HAPTIC,
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ object SettingsKeys {
|
|||||||
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
|
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
|
||||||
const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button"
|
const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button"
|
||||||
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
|
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
|
||||||
|
const val KEY_SHOW_ATTACHMENT_BUTTON = "show_attachment_button"
|
||||||
|
const val DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON = false
|
||||||
|
const val KEY_SHOW_RECORD_VOICE_BUTTON = "show_record_voice_button"
|
||||||
|
const val DEFAULT_VALUE_SHOW_RECORD_VOICE_BUTTON = false
|
||||||
|
|
||||||
const val KEY_APPEARANCE = "appearance"
|
const val KEY_APPEARANCE = "appearance"
|
||||||
const val KEY_APPEARANCE_MULTILINE = "appearance_multiline"
|
const val KEY_APPEARANCE_MULTILINE = "appearance_multiline"
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ interface UserSettings {
|
|||||||
val enableDynamicColors: StateFlow<Boolean>
|
val enableDynamicColors: StateFlow<Boolean>
|
||||||
val appLanguage: StateFlow<String>
|
val appLanguage: StateFlow<String>
|
||||||
|
|
||||||
val fastText: StateFlow<String>
|
|
||||||
|
|
||||||
val sendOnlineStatus: StateFlow<Boolean>
|
val sendOnlineStatus: StateFlow<Boolean>
|
||||||
|
|
||||||
val showAlertAfterCrash: StateFlow<Boolean>
|
|
||||||
val longPollInBackground: StateFlow<Boolean>
|
val longPollInBackground: StateFlow<Boolean>
|
||||||
val useBlur: StateFlow<Boolean>
|
val useBlur: StateFlow<Boolean>
|
||||||
val showEmojiButton: StateFlow<Boolean>
|
|
||||||
val showTimeInActionMessages: StateFlow<Boolean>
|
|
||||||
val useSystemFont: StateFlow<Boolean>
|
val useSystemFont: StateFlow<Boolean>
|
||||||
val enableAnimations: StateFlow<Boolean>
|
val enableAnimations: StateFlow<Boolean>
|
||||||
val showDebugCategory: StateFlow<Boolean>
|
val showDebugCategory: StateFlow<Boolean>
|
||||||
@@ -35,15 +30,10 @@ interface UserSettings {
|
|||||||
fun onEnableDynamicColorsChanged(enable: Boolean)
|
fun onEnableDynamicColorsChanged(enable: Boolean)
|
||||||
fun onAppLanguageChanged(language: String)
|
fun onAppLanguageChanged(language: String)
|
||||||
|
|
||||||
fun onFastTextChanged(text: String)
|
|
||||||
|
|
||||||
fun onSendOnlineStatusChanged(send: Boolean)
|
fun onSendOnlineStatusChanged(send: Boolean)
|
||||||
|
|
||||||
fun onShowAlertAfterCrashChanged(show: Boolean)
|
|
||||||
fun onLongPollInBackgroundChanged(inBackground: Boolean)
|
fun onLongPollInBackgroundChanged(inBackground: Boolean)
|
||||||
fun onUseBlurChanged(use: Boolean)
|
fun onUseBlurChanged(use: Boolean)
|
||||||
fun onShowEmojiButtonChanged(show: Boolean)
|
|
||||||
fun onShowTimeInActionMessagesChanged(show: Boolean)
|
|
||||||
fun onUseSystemFontChanged(use: Boolean)
|
fun onUseSystemFontChanged(use: Boolean)
|
||||||
fun onShowDebugCategoryChanged(show: Boolean)
|
fun onShowDebugCategoryChanged(show: Boolean)
|
||||||
}
|
}
|
||||||
@@ -58,16 +48,11 @@ class UserSettingsImpl : UserSettings {
|
|||||||
override val enableDynamicColors = MutableStateFlow(AppSettings.Appearance.enableDynamicColors)
|
override val enableDynamicColors = MutableStateFlow(AppSettings.Appearance.enableDynamicColors)
|
||||||
override val appLanguage = MutableStateFlow(AppSettings.Appearance.appLanguage)
|
override val appLanguage = MutableStateFlow(AppSettings.Appearance.appLanguage)
|
||||||
|
|
||||||
override val fastText = MutableStateFlow(AppSettings.Features.fastText)
|
|
||||||
|
|
||||||
override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus)
|
override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus)
|
||||||
|
|
||||||
override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash)
|
override val longPollInBackground =
|
||||||
override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground)
|
MutableStateFlow(AppSettings.Experimental.longPollInBackground)
|
||||||
override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur)
|
override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur)
|
||||||
override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton)
|
|
||||||
override val showTimeInActionMessages =
|
|
||||||
MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages)
|
|
||||||
override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont)
|
override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont)
|
||||||
override val enableAnimations = MutableStateFlow(AppSettings.Experimental.moreAnimations)
|
override val enableAnimations = MutableStateFlow(AppSettings.Experimental.moreAnimations)
|
||||||
override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory)
|
override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory)
|
||||||
@@ -96,18 +81,10 @@ class UserSettingsImpl : UserSettings {
|
|||||||
appLanguage.value = language
|
appLanguage.value = language
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFastTextChanged(text: String) {
|
|
||||||
fastText.value = text
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSendOnlineStatusChanged(send: Boolean) {
|
override fun onSendOnlineStatusChanged(send: Boolean) {
|
||||||
sendOnlineStatus.value = send
|
sendOnlineStatus.value = send
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowAlertAfterCrashChanged(show: Boolean) {
|
|
||||||
showAlertAfterCrash.value = show
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPollInBackgroundChanged(inBackground: Boolean) {
|
override fun onLongPollInBackgroundChanged(inBackground: Boolean) {
|
||||||
longPollInBackground.value = inBackground
|
longPollInBackground.value = inBackground
|
||||||
}
|
}
|
||||||
@@ -116,14 +93,6 @@ class UserSettingsImpl : UserSettings {
|
|||||||
useBlur.value = use
|
useBlur.value = use
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowEmojiButtonChanged(show: Boolean) {
|
|
||||||
showEmojiButton.value = show
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShowTimeInActionMessagesChanged(show: Boolean) {
|
|
||||||
showTimeInActionMessages.value = show
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUseSystemFontChanged(use: Boolean) {
|
override fun onUseSystemFontChanged(use: Boolean) {
|
||||||
useSystemFont.value = use
|
useSystemFont.value = use
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-6
@@ -5,13 +5,27 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.material3.ContainedLoadingIndicator
|
import androidx.compose.material3.ContainedLoadingIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.LoadingIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
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.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun FullScreenContainedLoader(modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.navigationBarsPadding(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ContainedLoader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun FullScreenLoader(modifier: Modifier = Modifier) {
|
fun FullScreenLoader(modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
@@ -20,15 +34,28 @@ fun FullScreenLoader(modifier: Modifier = Modifier) {
|
|||||||
.navigationBarsPadding(),
|
.navigationBarsPadding(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
Loader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun ContainedLoader(modifier: Modifier = Modifier) {
|
||||||
ContainedLoadingIndicator(
|
ContainedLoadingIndicator(
|
||||||
|
modifier = modifier,
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer
|
indicatorColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun Loader(modifier: Modifier = Modifier) {
|
||||||
|
LoadingIndicator(
|
||||||
|
modifier = modifier,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun FullScreenLoaderPreview() {
|
|
||||||
FullScreenLoader()
|
|
||||||
}
|
|
||||||
@@ -194,6 +194,7 @@
|
|||||||
<string name="settings_general_title">Основное</string>
|
<string name="settings_general_title">Основное</string>
|
||||||
<string name="settings_general_contact_names_title">Использовать имена контактов</string>
|
<string name="settings_general_contact_names_title">Использовать имена контактов</string>
|
||||||
<string name="settings_general_contact_names_summary">Приложение будет использовать доступные имена контактов для пользователей</string>
|
<string name="settings_general_contact_names_summary">Приложение будет использовать доступные имена контактов для пользователей</string>
|
||||||
|
<string name="settings_general_show_attachment_button_summary">Показывать кнопку вложений на панели чата</string>
|
||||||
<string name="settings_general_enable_haptic_title">Включить тактильную отдачу</string>
|
<string name="settings_general_enable_haptic_title">Включить тактильную отдачу</string>
|
||||||
<string name="settings_appearance_title">Внешний вид</string>
|
<string name="settings_appearance_title">Внешний вид</string>
|
||||||
<string name="settings_appearance_multiline_title">Многострочные заголовки и сообщения</string>
|
<string name="settings_appearance_multiline_title">Многострочные заголовки и сообщения</string>
|
||||||
@@ -272,4 +273,8 @@
|
|||||||
<string name="regular">Обычный</string>
|
<string name="regular">Обычный</string>
|
||||||
<string name="login_sign_up">Регистрация</string>
|
<string name="login_sign_up">Регистрация</string>
|
||||||
<string name="login_forgot_password">Забыли пароль?</string>
|
<string name="login_forgot_password">Забыли пароль?</string>
|
||||||
|
<string name="settings_general_show_attachment_button_title">Показывать кнопку вложений</string>
|
||||||
|
<string name="action_copy_link">Скопировать ссылку</string>
|
||||||
|
<string name="action_copy">Скопировать</string>
|
||||||
|
<string name="action_copy_image">Скопировать изображение</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -260,6 +260,8 @@
|
|||||||
<string name="settings_general_contact_names_summary">App will use available contact names for users</string>
|
<string name="settings_general_contact_names_summary">App will use available contact names for users</string>
|
||||||
<string name="settings_general_show_emoji_button_title">Show emoji button</string>
|
<string name="settings_general_show_emoji_button_title">Show emoji button</string>
|
||||||
<string name="settings_general_show_emoji_button_summary">Show emoji button in chat panel</string>
|
<string name="settings_general_show_emoji_button_summary">Show emoji button in chat panel</string>
|
||||||
|
<string name="settings_general_show_attachment_button_title">Show attachment button</string>
|
||||||
|
<string name="settings_general_show_attachment_button_summary">Show attachment button in chat panel</string>
|
||||||
<string name="settings_general_enable_haptic_title">Enable haptic</string>
|
<string name="settings_general_enable_haptic_title">Enable haptic</string>
|
||||||
<string name="settings_appearance_title">Appearance</string>
|
<string name="settings_appearance_title">Appearance</string>
|
||||||
<string name="settings_appearance_multiline_title">Multiline titles and messages</string>
|
<string name="settings_appearance_multiline_title">Multiline titles and messages</string>
|
||||||
@@ -348,4 +350,8 @@
|
|||||||
<string name="regular">Regular</string>
|
<string name="regular">Regular</string>
|
||||||
<string name="login_sign_up">Sign up</string>
|
<string name="login_sign_up">Sign up</string>
|
||||||
<string name="login_forgot_password">Forgot password?</string>
|
<string name="login_forgot_password">Forgot password?</string>
|
||||||
|
|
||||||
|
<string name="action_copy_link">Copy link</string>
|
||||||
|
<string name="action_copy">Copy</string>
|
||||||
|
<string name="action_copy_image">Copy image</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+2
-2
@@ -53,7 +53,7 @@ import dev.meloda.fast.model.BaseError
|
|||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
@@ -105,7 +105,7 @@ fun AudioMaterialsScreen(
|
|||||||
VkErrorView(baseError = baseError)
|
VkErrorView(baseError = baseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ import dev.meloda.fast.model.BaseError
|
|||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
@@ -114,7 +114,7 @@ fun FileMaterialsScreen(
|
|||||||
VkErrorView(baseError = baseError)
|
VkErrorView(baseError = baseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ import dev.meloda.fast.model.BaseError
|
|||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
@@ -114,7 +114,7 @@ fun LinkMaterialsScreen(
|
|||||||
VkErrorView(baseError = baseError)
|
VkErrorView(baseError = baseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
|
|||||||
+2
-2
@@ -46,7 +46,7 @@ import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
|||||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
@@ -98,7 +98,7 @@ fun PhotoMaterialsScreen(
|
|||||||
VkErrorView(baseError = baseError)
|
VkErrorView(baseError = baseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
|
|||||||
+2
-2
@@ -56,7 +56,7 @@ import dev.meloda.fast.model.BaseError
|
|||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
@@ -107,7 +107,7 @@ fun VideoMaterialsScreen(
|
|||||||
VkErrorView(baseError = baseError)
|
VkErrorView(baseError = baseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
|
|||||||
+2
-3
@@ -44,7 +44,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -65,7 +64,7 @@ import dev.chrisbanes.haze.materials.HazeMaterials
|
|||||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||||
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||||
@@ -306,7 +305,7 @@ fun ConversationsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && conversations.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && conversations.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
|||||||
+2
-4
@@ -51,7 +51,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -64,8 +63,7 @@ import dev.chrisbanes.haze.materials.HazeMaterials
|
|||||||
import dev.meloda.fast.conversations.CreateChatViewModel
|
import dev.meloda.fast.conversations.CreateChatViewModel
|
||||||
import dev.meloda.fast.conversations.model.CreateChatScreenState
|
import dev.meloda.fast.conversations.model.CreateChatScreenState
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.components.ErrorView
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
import dev.meloda.fast.ui.components.IconButton
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
@@ -271,7 +269,7 @@ fun CreateChatScreen(
|
|||||||
VkErrorView(baseError = baseError)
|
VkErrorView(baseError = baseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
|||||||
+2
-6
@@ -1,7 +1,6 @@
|
|||||||
package dev.meloda.fast.friends.presentation
|
package dev.meloda.fast.friends.presentation
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
@@ -28,11 +27,9 @@ import coil.imageLoader
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.meloda.fast.friends.FriendsViewModel
|
import dev.meloda.fast.friends.FriendsViewModel
|
||||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
|
||||||
import dev.meloda.fast.friends.OnlineFriendsViewModelImpl
|
|
||||||
import dev.meloda.fast.friends.navigation.Friends
|
import dev.meloda.fast.friends.navigation.Friends
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
@@ -41,7 +38,6 @@ import dev.meloda.fast.ui.theme.LocalThemeConfig
|
|||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import org.koin.androidx.compose.koinViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -136,7 +132,7 @@ fun FriendsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenContainedLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
|||||||
+55
-9
@@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory
|
|||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -15,9 +16,13 @@ import androidx.compose.ui.text.font.FontStyle
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||||
import com.conena.nanokt.text.isEmptyOrBlank
|
import com.conena.nanokt.text.isEmptyOrBlank
|
||||||
import com.conena.nanokt.text.isNotEmptyOrBlank
|
import com.conena.nanokt.text.isNotEmptyOrBlank
|
||||||
@@ -52,12 +57,16 @@ import dev.meloda.fast.model.LongPollParsedEvent
|
|||||||
import dev.meloda.fast.model.api.domain.FormatDataType
|
import dev.meloda.fast.model.api.domain.FormatDataType
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
||||||
import dev.meloda.fast.network.VkErrorCode
|
import dev.meloda.fast.network.VkErrorCode
|
||||||
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
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import dev.meloda.fast.ui.R as UiR
|
import dev.meloda.fast.ui.R as UiR
|
||||||
@@ -163,10 +172,6 @@ class MessagesHistoryViewModelImpl(
|
|||||||
updatesParser.onMessageMarkedAsImportant(::handleMessageMarkedAsImportant)
|
updatesParser.onMessageMarkedAsImportant(::handleMessageMarkedAsImportant)
|
||||||
updatesParser.onMessageMarkedAsSpam(::handleMessageMarkedAsSpam)
|
updatesParser.onMessageMarkedAsSpam(::handleMessageMarkedAsSpam)
|
||||||
updatesParser.onMessageMarkedAsNotSpam(::handleMessageMarkedAsNotSpam)
|
updatesParser.onMessageMarkedAsNotSpam(::handleMessageMarkedAsNotSpam)
|
||||||
|
|
||||||
userSettings.showTimeInActionMessages.listenValue(viewModelScope) {
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationConsumed() {
|
override fun onNavigationConsumed() {
|
||||||
@@ -1131,13 +1136,54 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyMessage(message: VkMessage) {
|
private fun copyMessage(message: VkMessage) {
|
||||||
val contentToCopy = message.text.orEmpty().trim()
|
|
||||||
if (contentToCopy.isEmpty()) return
|
|
||||||
|
|
||||||
val clipboardManager =
|
val clipboardManager =
|
||||||
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
||||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("Message", contentToCopy))
|
val messageToCopy = message.text.orEmpty().trim()
|
||||||
|
if (messageToCopy.isEmpty()) {
|
||||||
|
val photo = with(message.attachments.orEmpty()) {
|
||||||
|
if (size == 1 && all { it is VkPhotoDomain }) {
|
||||||
|
first() as? VkPhotoDomain
|
||||||
|
} else null
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
val photoMaxSize = photo.getMaxSize() ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val drawable = applicationContext.imageLoader.execute(
|
||||||
|
ImageRequest.Builder(applicationContext)
|
||||||
|
.data(photoMaxSize.url)
|
||||||
|
.build()
|
||||||
|
).drawable ?: return@launch
|
||||||
|
|
||||||
|
val imagesDir = File(applicationContext.cacheDir, "images")
|
||||||
|
if (!imagesDir.exists()) imagesDir.mkdirs()
|
||||||
|
val imageFile = File(imagesDir, "shared_image_id${photo.id}.png")
|
||||||
|
FileOutputStream(imageFile).use {
|
||||||
|
drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
applicationContext,
|
||||||
|
"${applicationContext.packageName}.provider",
|
||||||
|
imageFile
|
||||||
|
)
|
||||||
|
|
||||||
|
val clip = ClipData.newUri(applicationContext.contentResolver, "Image", uri)
|
||||||
|
clipboardManager.setPrimaryClip(clip)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
"Image copied to clipboard",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardManager.setPrimaryClip(ClipData.newPlainText("Message", messageToCopy))
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||||
Toast.makeText(applicationContext, UiR.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
Toast.makeText(applicationContext, UiR.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
||||||
@@ -1155,7 +1201,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
showName = false,
|
showName = false,
|
||||||
prevMessage = messages.getOrNull(index + 1),
|
prevMessage = messages.getOrNull(index + 1),
|
||||||
nextMessage = messages.getOrNull(index - 1),
|
nextMessage = messages.getOrNull(index - 1),
|
||||||
showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
|
showTimeInActionMessages = AppSettings.Experimental.showTimeInActionMessages,
|
||||||
conversation = screenState.value.conversation,
|
conversation = screenState.value.conversation,
|
||||||
isSelected = selectedMessages.indexOfFirstOrNull { it.id == message.id } != null
|
isSelected = selectedMessages.indexOfFirstOrNull { it.id == message.id } != null
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-2
@@ -5,6 +5,7 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
|
import dev.meloda.fast.common.model.UiImage
|
||||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryArguments
|
import dev.meloda.fast.messageshistory.model.MessagesHistoryArguments
|
||||||
import dev.meloda.fast.messageshistory.presentation.MessagesHistoryRoute
|
import dev.meloda.fast.messageshistory.presentation.MessagesHistoryRoute
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
@@ -27,13 +28,15 @@ data class MessagesHistory(val arguments: MessagesHistoryArguments) {
|
|||||||
fun NavGraphBuilder.messagesHistoryScreen(
|
fun NavGraphBuilder.messagesHistoryScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onNavigateToChatMaterials: (peerId: Long, cmId: Long) -> Unit
|
onNavigateToChatMaterials: (peerId: Long, cmId: Long) -> Unit,
|
||||||
|
onNavigateToPhotoViewer: (images: List<String>, index: Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
composable<MessagesHistory>(typeMap = MessagesHistory.typeMap) {
|
composable<MessagesHistory>(typeMap = MessagesHistory.typeMap) {
|
||||||
MessagesHistoryRoute(
|
MessagesHistoryRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onNavigateToChatMaterials = onNavigateToChatMaterials
|
onNavigateToChatMaterials = onNavigateToChatMaterials,
|
||||||
|
onNavigateToPhotoViewer = onNavigateToPhotoViewer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -16,6 +16,8 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.material3.MaterialTheme
|
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.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
@@ -38,6 +40,9 @@ fun IncomingMessageBubble(
|
|||||||
onClick: (VkAttachment) -> Unit = {},
|
onClick: (VkAttachment) -> Unit = {},
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
onLongClick: (VkAttachment) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val currentOnClick by rememberUpdatedState(onClick)
|
||||||
|
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -98,8 +103,8 @@ fun IncomingMessageBubble(
|
|||||||
isImportant = message.isImportant,
|
isImportant = message.isImportant,
|
||||||
isSelected = message.isSelected,
|
isSelected = message.isSelected,
|
||||||
attachments = message.attachments?.toImmutableList(),
|
attachments = message.attachments?.toImmutableList(),
|
||||||
onClick = onClick,
|
onClick = currentOnClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = currentOnLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -18,6 +18,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -50,6 +51,9 @@ fun MessageBubble(
|
|||||||
onClick: (VkAttachment) -> Unit = {},
|
onClick: (VkAttachment) -> Unit = {},
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
onLongClick: (VkAttachment) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val currentOnClick by rememberUpdatedState(onClick)
|
||||||
|
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||||
|
|
||||||
val theme = LocalThemeConfig.current
|
val theme = LocalThemeConfig.current
|
||||||
val backgroundColor = if (!isOut) {
|
val backgroundColor = if (!isOut) {
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
@@ -173,8 +177,8 @@ fun MessageBubble(
|
|||||||
Attachments(
|
Attachments(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
onClick = onClick,
|
onClick = currentOnClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = currentOnLongClick
|
||||||
)
|
)
|
||||||
|
|
||||||
val dateStatusBackground = if (theme.darkMode) Color.Black.copy(alpha = 0.5f)
|
val dateStatusBackground = if (theme.darkMode) Color.Black.copy(alpha = 0.5f)
|
||||||
|
|||||||
+7
-4
@@ -63,7 +63,9 @@ fun MessagesHistoryInputBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: TextFieldValue,
|
message: TextFieldValue,
|
||||||
hazeState: HazeState,
|
hazeState: HazeState,
|
||||||
|
enableHaptic: Boolean,
|
||||||
showEmojiButton: Boolean,
|
showEmojiButton: Boolean,
|
||||||
|
showAttachmentButton: Boolean,
|
||||||
actionMode: ActionMode,
|
actionMode: ActionMode,
|
||||||
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
||||||
onBoldRequested: () -> Unit = {},
|
onBoldRequested: () -> Unit = {},
|
||||||
@@ -179,7 +181,7 @@ fun MessagesHistoryInputBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.addTextContextMenuComponents {
|
.addTextContextMenuComponents {
|
||||||
separator()
|
separator()
|
||||||
@@ -236,17 +238,17 @@ fun MessagesHistoryInputBar(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (showAttachmentButton) {
|
||||||
val attachmentRotation = remember { Animatable(0f) }
|
val attachmentRotation = remember { Animatable(0f) }
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.Bottom) {
|
Column(verticalArrangement = Arrangement.Bottom) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onAttachmentButtonClicked()
|
onAttachmentButtonClicked()
|
||||||
if (AppSettings.General.enableHaptic) {
|
if (enableHaptic) {
|
||||||
view.performHapticFeedback(
|
view.performHapticFeedback(
|
||||||
HapticFeedbackConstantsCompat.REJECT
|
HapticFeedbackConstantsCompat.REJECT
|
||||||
)
|
)
|
||||||
@@ -277,6 +279,7 @@ fun MessagesHistoryInputBar(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val micRotation = remember { Animatable(0f) }
|
val micRotation = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -4,20 +4,21 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.meloda.fast.datastore.UserSettings
|
import dev.meloda.fast.common.model.UiImage
|
||||||
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel
|
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel
|
||||||
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
|
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
|
||||||
import dev.meloda.fast.messageshistory.model.MessageNavigation
|
import dev.meloda.fast.messageshistory.model.MessageNavigation
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesHistoryRoute(
|
fun MessagesHistoryRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onNavigateToChatMaterials: (peerId: Long, conversationMessageId: Long) -> Unit,
|
onNavigateToChatMaterials: (peerId: Long, conversationMessageId: Long) -> Unit,
|
||||||
|
onNavigateToPhotoViewer: (images: List<String>, index: Int) -> Unit,
|
||||||
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
@@ -30,9 +31,6 @@ fun MessagesHistoryRoute(
|
|||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||||
val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle()
|
val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val userSettings: UserSettings = koinInject()
|
|
||||||
val showEmojiButton by userSettings.showEmojiButton.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(navigationEvent) {
|
LaunchedEffect(navigationEvent) {
|
||||||
val needToConsume = when (val navigation = navigationEvent) {
|
val needToConsume = when (val navigation = navigationEvent) {
|
||||||
null -> false
|
null -> false
|
||||||
@@ -55,7 +53,9 @@ fun MessagesHistoryRoute(
|
|||||||
selectedMessages = selectedMessages.toImmutableList(),
|
selectedMessages = selectedMessages.toImmutableList(),
|
||||||
baseError = baseError,
|
baseError = baseError,
|
||||||
canPaginate = canPaginate,
|
canPaginate = canPaginate,
|
||||||
showEmojiButton = showEmojiButton,
|
showEmojiButton = AppSettings.General.showEmojiButton,
|
||||||
|
showAttachmentButton = AppSettings.General.showAttachmentButton,
|
||||||
|
enableHaptic = AppSettings.General.enableHaptic,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onClose = viewModel::onCloseButtonClicked,
|
onClose = viewModel::onCloseButtonClicked,
|
||||||
onScrolledToIndex = viewModel::onScrolledToIndex,
|
onScrolledToIndex = viewModel::onScrolledToIndex,
|
||||||
@@ -69,6 +69,7 @@ fun MessagesHistoryRoute(
|
|||||||
onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked,
|
onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked,
|
||||||
onMessageClicked = viewModel::onMessageClicked,
|
onMessageClicked = viewModel::onMessageClicked,
|
||||||
onMessageLongClicked = viewModel::onMessageLongClicked,
|
onMessageLongClicked = viewModel::onMessageLongClicked,
|
||||||
|
onPhotoClicked = onNavigateToPhotoViewer,
|
||||||
onPinnedMessageClicked = viewModel::onPinnedMessageClicked,
|
onPinnedMessageClicked = viewModel::onPinnedMessageClicked,
|
||||||
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked,
|
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked,
|
||||||
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked,
|
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked,
|
||||||
|
|||||||
+9
-3
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -45,6 +44,7 @@ import dev.meloda.fast.messageshistory.model.UiItem
|
|||||||
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
|
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import dev.meloda.fast.ui.components.Loader
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
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
|
||||||
@@ -68,6 +68,8 @@ fun MessagesHistoryScreen(
|
|||||||
baseError: BaseError? = null,
|
baseError: BaseError? = null,
|
||||||
canPaginate: Boolean = false,
|
canPaginate: Boolean = false,
|
||||||
showEmojiButton: Boolean = false,
|
showEmojiButton: Boolean = false,
|
||||||
|
showAttachmentButton: Boolean = false,
|
||||||
|
enableHaptic: Boolean = false,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
onClose: () -> Unit = {},
|
onClose: () -> Unit = {},
|
||||||
onScrolledToIndex: () -> Unit = {},
|
onScrolledToIndex: () -> Unit = {},
|
||||||
@@ -81,6 +83,7 @@ fun MessagesHistoryScreen(
|
|||||||
onEmojiButtonLongClicked: () -> Unit = {},
|
onEmojiButtonLongClicked: () -> Unit = {},
|
||||||
onMessageClicked: (Long) -> Unit = {},
|
onMessageClicked: (Long) -> Unit = {},
|
||||||
onMessageLongClicked: (Long) -> Unit = {},
|
onMessageLongClicked: (Long) -> Unit = {},
|
||||||
|
onPhotoClicked: (images: List<String>, index: Int) -> Unit = { _, _ -> },
|
||||||
onPinnedMessageClicked: (Long) -> Unit = {},
|
onPinnedMessageClicked: (Long) -> Unit = {},
|
||||||
onUnpinMessageButtonClicked: () -> Unit = {},
|
onUnpinMessageButtonClicked: () -> Unit = {},
|
||||||
onDeleteSelectedButtonClicked: () -> Unit = {},
|
onDeleteSelectedButtonClicked: () -> Unit = {},
|
||||||
@@ -231,7 +234,8 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
currentOnMessageClicked.invoke(id)
|
currentOnMessageClicked.invoke(id)
|
||||||
},
|
},
|
||||||
onMessageLongClicked = onMessageLongClicked
|
onMessageLongClicked = onMessageLongClicked,
|
||||||
|
onPhotoClicked = onPhotoClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
MessagesHistoryInputBar(
|
MessagesHistoryInputBar(
|
||||||
@@ -244,7 +248,9 @@ fun MessagesHistoryScreen(
|
|||||||
onLinkRequested = onLinkRequested,
|
onLinkRequested = onLinkRequested,
|
||||||
onRegularRequested = onRegularRequested,
|
onRegularRequested = onRegularRequested,
|
||||||
hazeState = hazeState,
|
hazeState = hazeState,
|
||||||
|
enableHaptic = enableHaptic,
|
||||||
showEmojiButton = showEmojiButton,
|
showEmojiButton = showEmojiButton,
|
||||||
|
showAttachmentButton = showAttachmentButton,
|
||||||
actionMode = screenState.actionMode,
|
actionMode = screenState.actionMode,
|
||||||
onSetMessageBarHeight = { messageBarHeight = it },
|
onSetMessageBarHeight = { messageBarHeight = it },
|
||||||
onEmojiButtonLongClicked = onEmojiButtonLongClicked,
|
onEmojiButtonLongClicked = onEmojiButtonLongClicked,
|
||||||
@@ -254,7 +260,7 @@ fun MessagesHistoryScreen(
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
screenState.isLoading && messages.values.isEmpty() -> {
|
screenState.isLoading && messages.values.isEmpty() -> {
|
||||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
Loader(modifier = Modifier.align(Alignment.Center))
|
||||||
}
|
}
|
||||||
|
|
||||||
baseError != null -> {
|
baseError != null -> {
|
||||||
|
|||||||
+23
-11
@@ -23,7 +23,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -56,25 +56,36 @@ fun MessagesList(
|
|||||||
messageBarHeight: Dp,
|
messageBarHeight: Dp,
|
||||||
onRequestScrollToCmId: (cmId: Long) -> Unit = {},
|
onRequestScrollToCmId: (cmId: Long) -> Unit = {},
|
||||||
onMessageClicked: (Long) -> Unit = {},
|
onMessageClicked: (Long) -> Unit = {},
|
||||||
onMessageLongClicked: (Long) -> Unit = {}
|
onMessageLongClicked: (Long) -> Unit = {},
|
||||||
|
onPhotoClicked: (images: List<String>, index: Int) -> Unit = { _, _ -> }
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val theme = LocalThemeConfig.current
|
val theme = LocalThemeConfig.current
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
||||||
val onAttachmentClick = remember {
|
val onAttachmentClick by rememberUpdatedState(
|
||||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
{ message: UiItem.Message, attachment: VkAttachment ->
|
||||||
if (isSelectedAtLeastOne) {
|
if (isSelectedAtLeastOne) {
|
||||||
onMessageClicked(message.id)
|
onMessageClicked(message.id)
|
||||||
} else {
|
} else {
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is VkPhotoDomain -> {
|
is VkPhotoDomain -> {
|
||||||
val maxSize = attachment.getMaxSize()
|
val photos = message.attachments
|
||||||
maxSize?.let {
|
.orEmpty()
|
||||||
context.startActivity(
|
.filterIsInstance<VkPhotoDomain>()
|
||||||
Intent(Intent.ACTION_VIEW, maxSize.url.toUri())
|
.mapNotNull { photo -> photo.getMaxSize()?.url }
|
||||||
|
|
||||||
|
onPhotoClicked(
|
||||||
|
photos,
|
||||||
|
photos.indexOfFirst { it == attachment.getMaxSize()?.url }
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
// val maxSize = attachment.getMaxSize()
|
||||||
|
// maxSize?.let {
|
||||||
|
// context.startActivity(
|
||||||
|
// Intent(Intent.ACTION_VIEW, maxSize.url.toUri())
|
||||||
|
// )
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
is VkFileDomain -> {
|
is VkFileDomain -> {
|
||||||
@@ -91,9 +102,9 @@ fun MessagesList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
val onAttachmentLongClick = remember {
|
val onAttachmentLongClick by rememberUpdatedState(
|
||||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
{ message: UiItem.Message, attachment: VkAttachment ->
|
||||||
if (isSelectedAtLeastOne) {
|
if (isSelectedAtLeastOne) {
|
||||||
onMessageLongClicked(message.id)
|
onMessageLongClicked(message.id)
|
||||||
@@ -107,7 +118,7 @@ fun MessagesList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -200,6 +211,7 @@ fun MessagesList(
|
|||||||
),
|
),
|
||||||
message = item,
|
message = item,
|
||||||
onClick = { attachment ->
|
onClick = { attachment ->
|
||||||
|
|
||||||
onAttachmentClick(item, attachment)
|
onAttachmentClick(item, attachment)
|
||||||
},
|
},
|
||||||
onLongClick = { attachment ->
|
onLongClick = { attachment ->
|
||||||
|
|||||||
+88
-3
@@ -1,21 +1,43 @@
|
|||||||
package dev.meloda.fast.photoviewer
|
package dev.meloda.fast.photoviewer
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import coil.imageLoader
|
||||||
|
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.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 kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
interface PhotoViewViewModel {
|
interface PhotoViewViewModel {
|
||||||
val screenState: StateFlow<PhotoViewScreenState>
|
val screenState: StateFlow<PhotoViewScreenState>
|
||||||
|
|
||||||
|
fun onPageChanged(newPage: Int)
|
||||||
|
|
||||||
|
fun onCopyLinkClicked()
|
||||||
|
fun onCopyClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
class PhotoViewViewModelImpl(
|
class PhotoViewViewModelImpl(
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle,
|
||||||
|
private val applicationContext: Context
|
||||||
) : PhotoViewViewModel, ViewModel() {
|
) : PhotoViewViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
||||||
@@ -25,10 +47,73 @@ class PhotoViewViewModelImpl(
|
|||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
images = arguments.images
|
images = arguments.imageUrls
|
||||||
.map { URLDecoder.decode(it, "utf-8") }
|
.map { URLDecoder.decode(it, "utf-8") }
|
||||||
.map(UiImage::Url)
|
.map(UiImage::Url),
|
||||||
|
selectedPage = arguments.selectedIndex?.takeIf { it != -1 } ?: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPageChanged(newPage: Int) {
|
||||||
|
screenState.setValue { old -> old.copy(selectedPage = newPage) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCopyLinkClicked() {
|
||||||
|
val url = screenState.value.images
|
||||||
|
.getOrNull(screenState.value.selectedPage)
|
||||||
|
?.extractUrl() ?: return
|
||||||
|
|
||||||
|
val clipboardManager =
|
||||||
|
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
||||||
|
clipboardManager.setPrimaryClip(ClipData.newPlainText("URL", url))
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
"URL copied to clipboard",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCopyClicked() {
|
||||||
|
val clipboardManager =
|
||||||
|
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
||||||
|
val url = screenState.value.images
|
||||||
|
.getOrNull(screenState.value.selectedPage)
|
||||||
|
?.extractUrl() ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val drawable = applicationContext.imageLoader.execute(
|
||||||
|
ImageRequest.Builder(applicationContext)
|
||||||
|
.data(url)
|
||||||
|
.build()
|
||||||
|
).drawable ?: return@launch
|
||||||
|
|
||||||
|
val imagesDir = File(applicationContext.cacheDir, "images")
|
||||||
|
if (!imagesDir.exists()) imagesDir.mkdirs()
|
||||||
|
val imageFile = File(imagesDir, "shared_image_id${UUID.randomUUID()}.png")
|
||||||
|
FileOutputStream(imageFile).use {
|
||||||
|
drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
applicationContext,
|
||||||
|
"${applicationContext.packageName}.provider",
|
||||||
|
imageFile
|
||||||
|
)
|
||||||
|
|
||||||
|
val clip = ClipData.newUri(applicationContext.contentResolver, "Image", uri)
|
||||||
|
clipboardManager.setPrimaryClip(clip)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
"Image copied to clipboard",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -7,5 +7,6 @@ import kotlinx.serialization.Serializable
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PhotoViewArguments(
|
data class PhotoViewArguments(
|
||||||
val images: List<String>
|
val imageUrls: List<String>,
|
||||||
|
val selectedIndex: Int?
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|||||||
+4
-2
@@ -5,12 +5,14 @@ import dev.meloda.fast.common.model.UiImage
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class PhotoViewScreenState(
|
data class PhotoViewScreenState(
|
||||||
val images: List<UiImage>
|
val images: List<UiImage>,
|
||||||
|
val selectedPage: Int
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY: PhotoViewScreenState = PhotoViewScreenState(
|
val EMPTY: PhotoViewScreenState = PhotoViewScreenState(
|
||||||
images = emptyList()
|
images = emptyList(),
|
||||||
|
selectedPage = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -30,11 +30,15 @@ fun NavGraphBuilder.photoViewScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavController.navigateToPhotoView(images: List<String>) {
|
fun NavController.navigateToPhotoView(
|
||||||
|
images: List<String>,
|
||||||
|
selectedIndex: Int? = null
|
||||||
|
) {
|
||||||
this.navigate(
|
this.navigate(
|
||||||
PhotoView(
|
PhotoView(
|
||||||
arguments = PhotoViewArguments(
|
arguments = PhotoViewArguments(
|
||||||
images.map { URLEncoder.encode(it, "utf-8") }
|
imageUrls = images.map { URLEncoder.encode(it, "utf-8") },
|
||||||
|
selectedIndex = selectedIndex
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+77
-31
@@ -7,6 +7,7 @@ import androidx.compose.foundation.gestures.draggable
|
|||||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
@@ -14,27 +15,36 @@ import androidx.compose.foundation.pager.PagerState
|
|||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.conena.nanokt.android.content.pxToDp
|
import com.conena.nanokt.android.content.pxToDp
|
||||||
@@ -56,16 +66,30 @@ fun PhotoViewRoute(
|
|||||||
|
|
||||||
PhotoViewScreen(
|
PhotoViewScreen(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
onBack = onBack
|
onBack = onBack,
|
||||||
|
onPageChanged = viewModel::onPageChanged,
|
||||||
|
onCopyLinkClicked = viewModel::onCopyLinkClicked,
|
||||||
|
onCopyClicked = viewModel::onCopyClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PhotoViewScreen(
|
fun PhotoViewScreen(
|
||||||
screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY,
|
screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY,
|
||||||
onBack: () -> Unit = {}
|
onBack: () -> Unit = {},
|
||||||
|
onPageChanged: (index: Int) -> Unit = {},
|
||||||
|
onCopyLinkClicked: () -> Unit = {},
|
||||||
|
onCopyClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState(pageCount = { screenState.images.size })
|
val pagerState = rememberPagerState(
|
||||||
|
pageCount = { screenState.images.size },
|
||||||
|
initialPage = screenState.selectedPage
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(pagerState) {
|
||||||
|
snapshotFlow { pagerState.currentPage }
|
||||||
|
.collect(onPageChanged)
|
||||||
|
}
|
||||||
|
|
||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
@@ -81,7 +105,13 @@ fun PhotoViewScreen(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.graphicsLayer(alpha = calculatedAlpha),
|
modifier = Modifier.graphicsLayer(alpha = calculatedAlpha),
|
||||||
topBar = { TopBar(onBack = onBack) },
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
onBack = onBack,
|
||||||
|
onCopyClicked = onCopyClicked,
|
||||||
|
onCopyLinkClicked = onCopyLinkClicked,
|
||||||
|
)
|
||||||
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.background.copy(
|
containerColor = MaterialTheme.colorScheme.background.copy(
|
||||||
alpha = calculatedAlpha
|
alpha = calculatedAlpha
|
||||||
)
|
)
|
||||||
@@ -103,14 +133,18 @@ fun PhotoViewScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
fun TopBar(
|
fun TopBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit,
|
||||||
|
onCopyClicked: () -> Unit,
|
||||||
|
onCopyLinkClicked: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var dropdownMenuShown by remember {
|
var dropdownMenuShown by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hideDropDownMenu by rememberUpdatedState(
|
||||||
|
{ dropdownMenuShown = false }
|
||||||
|
)
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = {},
|
title = {},
|
||||||
@@ -123,29 +157,40 @@ fun TopBar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
// IconButton.kt(
|
IconButton(
|
||||||
// onClick = { dropdownMenuShown = true }
|
onClick = { dropdownMenuShown = true }
|
||||||
// ) {
|
) {
|
||||||
// Icon(
|
Icon(
|
||||||
// imageVector = Icons.Rounded.MoreVert,
|
imageVector = Icons.Rounded.MoreVert,
|
||||||
// contentDescription = "Options"
|
contentDescription = "Options"
|
||||||
// )
|
)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// DropdownMenu(
|
DropdownMenu(
|
||||||
// modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||||
// expanded = dropdownMenuShown,
|
expanded = dropdownMenuShown,
|
||||||
// onDismissRequest = { dropdownMenuShown = false },
|
onDismissRequest = { dropdownMenuShown = false },
|
||||||
// offset = DpOffset(x = (10).dp, y = (-60).dp)
|
offset = DpOffset(x = (10).dp, y = (-60).dp)
|
||||||
// ) {
|
) {
|
||||||
// DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
// onClick = {
|
onClick = {
|
||||||
// Toast.makeText(context, "Save clicked", Toast.LENGTH_SHORT).show()
|
hideDropDownMenu()
|
||||||
// dropdownMenuShown = false
|
onCopyLinkClicked()
|
||||||
// },
|
},
|
||||||
// text = { Text(text = "Save") },
|
text = {
|
||||||
// )
|
Text(text = stringResource(UiR.string.action_copy_link))
|
||||||
// }
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
hideDropDownMenu()
|
||||||
|
onCopyClicked()
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(UiR.string.action_copy_image))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
||||||
)
|
)
|
||||||
@@ -230,7 +275,8 @@ private fun PhotoViewScreenPreview() {
|
|||||||
screenState = PhotoViewScreenState(
|
screenState = PhotoViewScreenState(
|
||||||
images = List(200) {
|
images = List(200) {
|
||||||
UiImage.Resource(UiR.drawable.test_captcha)
|
UiImage.Resource(UiR.drawable.test_captcha)
|
||||||
}
|
},
|
||||||
|
selectedPage = 0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,24 +201,12 @@ class SettingsViewModelImpl(
|
|||||||
userSettings.onAppLanguageChanged(newLanguage)
|
userSettings.onAppLanguageChanged(newLanguage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT -> {
|
|
||||||
val newText = newValue as? String ?: SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT
|
|
||||||
userSettings.onFastTextChanged(newText)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
|
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> {
|
||||||
val isUsing = newValue as? Boolean
|
val isUsing = newValue as? Boolean
|
||||||
?: SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS
|
?: SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS
|
||||||
userSettings.onSendOnlineStatusChanged(isUsing)
|
userSettings.onSendOnlineStatusChanged(isUsing)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT -> {
|
|
||||||
val show = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
|
||||||
userSettings.onShowAlertAfterCrashChanged(show)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND -> {
|
SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND -> {
|
||||||
val inBackground = newValue as? Boolean
|
val inBackground = newValue as? Boolean
|
||||||
?: SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND
|
?: SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND
|
||||||
@@ -240,17 +228,6 @@ class SettingsViewModelImpl(
|
|||||||
userSettings.onUseBlurChanged(isUsing)
|
userSettings.onUseBlurChanged(isUsing)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsKeys.KEY_SHOW_EMOJI_BUTTON -> {
|
|
||||||
val show = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
|
||||||
userSettings.onShowEmojiButtonChanged(show)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES -> {
|
|
||||||
val show = newValue as? Boolean
|
|
||||||
?: SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES
|
|
||||||
userSettings.onShowTimeInActionMessagesChanged(show)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsKeys.KEY_USE_SYSTEM_FONT -> {
|
SettingsKeys.KEY_USE_SYSTEM_FONT -> {
|
||||||
val use = newValue as? Boolean ?: SettingsKeys.DEFAULT_USE_SYSTEM_FONT
|
val use = newValue as? Boolean ?: SettingsKeys.DEFAULT_USE_SYSTEM_FONT
|
||||||
userSettings.onUseSystemFontChanged(use)
|
userSettings.onUseSystemFontChanged(use)
|
||||||
@@ -302,6 +279,12 @@ class SettingsViewModelImpl(
|
|||||||
text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary),
|
text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary),
|
||||||
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
||||||
)
|
)
|
||||||
|
val generalShowAttachmentButton = SettingsItem.Switch(
|
||||||
|
key = SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON,
|
||||||
|
title = UiText.Resource(UiR.string.settings_general_show_attachment_button_title),
|
||||||
|
text = UiText.Resource(UiR.string.settings_general_show_attachment_button_summary),
|
||||||
|
defaultValue = SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON
|
||||||
|
)
|
||||||
val generalEnableHaptic = SettingsItem.Switch(
|
val generalEnableHaptic = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_ENABLE_HAPTIC,
|
key = SettingsKeys.KEY_ENABLE_HAPTIC,
|
||||||
defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC,
|
defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC,
|
||||||
@@ -476,6 +459,7 @@ class SettingsViewModelImpl(
|
|||||||
generalTitle,
|
generalTitle,
|
||||||
generalUseContactNames,
|
generalUseContactNames,
|
||||||
generalShowEmojiButton,
|
generalShowEmojiButton,
|
||||||
|
generalShowAttachmentButton,
|
||||||
generalEnableHaptic
|
generalEnableHaptic
|
||||||
)
|
)
|
||||||
val appearanceList = listOf(
|
val appearanceList = listOf(
|
||||||
|
|||||||
Reference in New Issue
Block a user