3 Commits

Author SHA1 Message Date
melod1n 0682d6c42c version up 2024-12-17 20:52:22 +03:00
melod1n 6a69f28256 ability to use more animations (experimental);
fix online friends loading;
conversation avatar in messages history screen;
test second tap on conversations item in bottom bar to scroll to top;
etc
2024-12-17 20:51:02 +03:00
melod1n 85cda2065e settings reorganization;
implement long press on emoji button for fast text;
some deprecations fixed;
some typos fixed;
etc
2024-12-17 12:53:02 +03:00
31 changed files with 292 additions and 501 deletions
+2 -2
View File
@@ -7,10 +7,10 @@ plugins {
} }
android { android {
namespace = "dev.meloda.fastvk" namespace = "dev.meloda.fast"
defaultConfig { defaultConfig {
applicationId = "dev.meloda.fastvk" applicationId = "dev.meloda.fast"
versionCode = libs.versions.versionCode.get().toInt() versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get() versionName = libs.versions.versionName.get()
+3 -3
View File
@@ -22,7 +22,7 @@
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name="dev.meloda.fast.presentation.MainActivity" android:name=".presentation.MainActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
@@ -38,13 +38,13 @@
</activity> </activity>
<service <service
android:name="dev.meloda.fast.service.longpolling.LongPollingService" android:name=".service.longpolling.LongPollingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service <service
android:name="dev.meloda.fast.service.OnlineService" android:name=".service.OnlineService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
@@ -86,8 +86,6 @@ class MainViewModelImpl(
BaseError.SessionExpired -> { BaseError.SessionExpired -> {
isNeedToReplaceWithAuth.update { true } isNeedToReplaceWithAuth.update { true }
} }
is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
} }
} }
@@ -26,8 +26,8 @@ import dev.meloda.fast.provider.ApiLanguageProvider
import dev.meloda.fast.service.longpolling.di.longPollModule import dev.meloda.fast.service.longpolling.di.longPollModule
import dev.meloda.fast.settings.di.settingsModule import dev.meloda.fast.settings.di.settingsModule
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.qualifier import org.koin.core.qualifier.qualifier
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
@@ -61,7 +61,7 @@ val applicationModule = module {
qualifier = qualifier("main") qualifier = qualifier("main")
} }
single<ImageLoader> { single {
ImageLoader.Builder(get()) ImageLoader.Builder(get())
.crossfade(true) .crossfade(true)
.build() .build()
@@ -95,6 +95,11 @@ class OnlineService : Service() {
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } } }.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
} }
override fun onLowMemory() {
Log.d(STATE_TAG, "onLowMemory")
super.onLowMemory()
}
override fun onDestroy() { override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy") Log.d(STATE_TAG, "onDestroy")
@@ -1,5 +1,7 @@
package dev.meloda.fast.common.extensions package dev.meloda.fast.common.extensions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -86,7 +86,7 @@ object AndroidUtils {
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Settings.ACTION_SECURITY_SETTINGS Settings.ACTION_SECURITY_SETTINGS
} else { } else {
data = Uri.parse("package:dev.meloda.fastvk") data = Uri.parse("package:dev.meloda.fast")
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
} }
}) })
@@ -20,20 +20,18 @@ sealed class State<out T> {
data object ConnectionError : Error() data object ConnectionError : Error()
data object UnknownError : Error() data object Unknown : Error()
data object InternalError : Error() data object InternalError : Error()
data class OAuthError(val error: OAuthErrorDomain) : Error() data class OAuthError(val error: OAuthErrorDomain) : Error()
data class TestError(val message: String) : Error()
} }
fun isLoading(): Boolean = this is Loading fun isLoading(): Boolean = this is Loading
companion object { companion object {
val UNKNOWN_ERROR = Error.UnknownError val UNKNOWN_ERROR = Error.Unknown
} }
} }
@@ -75,12 +73,11 @@ fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
} }
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = when (this) {
when (this) {
is ApiResult.Success -> State.Success(successMapper(this.value)) is ApiResult.Success -> State.Success(successMapper(this.value))
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError() is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
} }
@@ -33,7 +33,6 @@ class OAuthUseCaseImpl(
forceSms = forceSms forceSms = forceSms
) )
kotlin.runCatching {
val error = response.error?.let(VkOAuthError::parse) val error = response.error?.let(VkOAuthError::parse)
val errorType = response.errorType?.let(VkOAuthErrorType::parse) val errorType = response.errorType?.let(VkOAuthErrorType::parse)
@@ -121,12 +120,5 @@ class OAuthUseCaseImpl(
} }
emit(newState) emit(newState)
}.fold(
onSuccess = {
},
onFailure = {
emit(State.Error.TestError(it.stackTraceToString()))
}
)
} }
} }
@@ -6,6 +6,4 @@ import androidx.compose.runtime.Immutable
sealed class BaseError { sealed class BaseError {
data object SessionExpired : BaseError() data object SessionExpired : BaseError()
data class SimpleError(val message: String) : BaseError()
} }
@@ -1,7 +1,7 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkVideoDomain import dev.meloda.fast.model.api.domain.VkVideoDomain
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkVideoData( data class VkVideoData(
@@ -12,7 +12,7 @@ data class VkVideoData(
val duration: Int, val duration: Int,
val date: Int, val date: Int,
val comments: Int?, val comments: Int?,
val description: String?, val description: String,
val player: String?, val player: String?,
val added: Int?, val added: Int?,
val type: String, val type: String,
@@ -20,9 +20,9 @@ data class VkVideoData(
val access_key: String?, val access_key: String?,
val owner_id: Int, val owner_id: Int,
val is_favorite: Boolean?, val is_favorite: Boolean?,
val image: List<Image>?, val image: List<Image>,
val first_frame: List<FirstFrame>?, val first_frame: List<FirstFrame>?,
val files: File? val files: File?,
) : VkAttachmentData { ) : VkAttachmentData {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -67,7 +67,7 @@ data class VkVideoData(
fun toDomain() = VkVideoDomain( fun toDomain() = VkVideoDomain(
id = id, id = id,
ownerId = owner_id, ownerId = owner_id,
images = image.orEmpty().map { it.asVideoImage() }, images = image.map { it.asVideoImage() },
firstFrames = first_frame, firstFrames = first_frame,
accessKey = access_key, accessKey = access_key,
title = title title = title
@@ -46,13 +46,8 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
return successModel return successModel
}, },
onFailure = { failure -> onFailure = { failure ->
if (failure is JsonDataException) { if(failure is JsonDataException) {
throw ApiException( throw failure
RestApiError(
errorCode = -1,
errorMsg = failure.message.orEmpty()
)
)
} }
val isUnit = successType == Unit::class.java val isUnit = successType == Unit::class.java
@@ -5,8 +5,6 @@ enum class ValidationType(val value: String) {
SMS("2fa_sms"); SMS("2fa_sms");
companion object { companion object {
fun parse(value: String): ValidationType = fun parse(value: String): ValidationType = entries.first { it.value == value }
entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown validation type $value")
} }
} }
@@ -1,7 +1,6 @@
package dev.meloda.fast.network package dev.meloda.fast.network
enum class VkErrorCode(val code: Int) { enum class VkErrorCode(val code: Int) {
WTF(-1),
UNKNOWN_ERROR(1), UNKNOWN_ERROR(1),
APP_DISABLED(2), APP_DISABLED(2),
UNKNOWN_METHOD(3), UNKNOWN_METHOD(3),
@@ -6,6 +6,7 @@ import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory
import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dev.meloda.fast.common.AppConstants import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.common.model.LogLevel
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.network.JsonConverter import dev.meloda.fast.network.JsonConverter
import dev.meloda.fast.network.MoshiConverter import dev.meloda.fast.network.MoshiConverter
@@ -56,8 +57,12 @@ val networkModule = module {
.followSslRedirects(true) .followSslRedirects(true)
.addInterceptor( .addInterceptor(
HttpLoggingInterceptor().apply { HttpLoggingInterceptor().apply {
level = level = when (AppSettings.Debug.networkLogLevel) {
HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal] LogLevel.NONE -> HttpLoggingInterceptor.Level.NONE
LogLevel.BASIC -> HttpLoggingInterceptor.Level.BASIC
LogLevel.HEADERS -> HttpLoggingInterceptor.Level.HEADERS
LogLevel.BODY -> HttpLoggingInterceptor.Level.BODY
}
} }
) )
.build() .build()
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
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.Text import androidx.compose.material3.Text
@@ -23,9 +22,7 @@ fun ErrorView(
onButtonClick: (() -> Unit)? = null, onButtonClick: (() -> Unit)? = null,
) { ) {
Column( Column(
modifier = modifier modifier = modifier.fillMaxSize(),
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@@ -133,7 +133,6 @@
<string name="message_attachment_story_your_story">Ваша история</string> <string name="message_attachment_story_your_story">Ваша история</string>
<string name="settings_dynamic_colors">Динамические цвета</string> <string name="settings_dynamic_colors">Динамические цвета</string>
<string name="settings_dynamic_colors_description">Цвета для приложения будут извлечены из ваших обоев на главном экране</string> <string name="settings_dynamic_colors_description">Цвета для приложения будут извлечены из ваших обоев на главном экране</string>
<string name="settings_appearance_use_system_font_title">Использовать системный шрифт</string>
<string name="settings_application_language">Язык приложения</string> <string name="settings_application_language">Язык приложения</string>
<string name="settings_application_language_value">Текущий: %1$s</string> <string name="settings_application_language_value">Текущий: %1$s</string>
<string name="language_system">Системный</string> <string name="language_system">Системный</string>
@@ -178,7 +177,6 @@
<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_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>
<string name="settings_appearance_multiline_summary">Заголовок чата и текст сообщения смогут занимать несколько строчек</string> <string name="settings_appearance_multiline_summary">Заголовок чата и текст сообщения смогут занимать несколько строчек</string>
@@ -186,11 +184,9 @@
<string name="settings_features_fast_text_title">Fast текст</string> <string name="settings_features_fast_text_title">Fast текст</string>
<string name="settings_features_long_poll_in_background_title">LongPoll в фоне</string> <string name="settings_features_long_poll_in_background_title">LongPoll в фоне</string>
<string name="settings_features_long_poll_in_background_summary">Ваши сообщения будут обновляться, даже если приложение находится в фоне</string> <string name="settings_features_long_poll_in_background_summary">Ваши сообщения будут обновляться, даже если приложение находится в фоне</string>
<string name="settings_experimental_more_animations_summary">Использовать анимации везде, где возможно</string>
<string name="settings_activity_title">Активность</string> <string name="settings_activity_title">Активность</string>
<string name="settings_activity_send_online_title">Быть «в сети»</string> <string name="settings_activity_send_online_title">Быть «в сети»</string>
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string> <string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string>
<string name="settings_experimental_title">Экспериментальные - ОЧЕНЬ нестабильные</string>
<string name="settings_debug_title">Отладка</string> <string name="settings_debug_title">Отладка</string>
<string name="action_disable">Отключить</string> <string name="action_disable">Отключить</string>
<string name="background_long_poll_rationale_text">Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям</string> <string name="background_long_poll_rationale_text">Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям</string>
@@ -202,14 +198,4 @@
<string name="notification_channel_no_category_description">Уведомления без категории</string> <string name="notification_channel_no_category_description">Уведомления без категории</string>
<string name="notification_channel_long_polling_service_name">Сервис обновления сообщений</string> <string name="notification_channel_long_polling_service_name">Сервис обновления сообщений</string>
<string name="notification_channel_long_polling_service_description">Уведомления сервиса обновлений сообщений</string> <string name="notification_channel_long_polling_service_description">Уведомления сервиса обновлений сообщений</string>
<string name="settings_general_show_emoji_button_title">Показывать кнопку эмоджи</string>
<string name="settings_general_show_emoji_button_summary">Показывать кнопку эмоджи на панели чата</string>
<string name="settings_features_show_time_in_action_messages_title">Показывать время в сервисных сообщениях</string>
<string name="settings_experimental_use_blur_title">Использовать размытие</string>
<string name="settings_experimental_use_blur_summary">Добавлять размытие везде, где возможно.\\nРаботает только с 12 версии Android</string>
<string name="settings_experimental_more_animations_title">Больше анимаций</string>
<string name="warning_confirmation">Подтверждение</string>
<string name="captcha_exit_warning">Вы уверены? Процесс ввода капчи будет отменён</string>
<string name="validation_exit_warning">Вы уверены? Процесс ввода кода-подтверждения будет отменён</string>
<string name="action_authorize">Авторизоваться</string>
</resources> </resources>
-14
View File
@@ -204,8 +204,6 @@
<string name="settings_dynamic_colors">Dynamic colors</string> <string name="settings_dynamic_colors">Dynamic colors</string>
<string name="settings_dynamic_colors_description">The colors for the app will be extracted from your home screen wallpaper</string> <string name="settings_dynamic_colors_description">The colors for the app will be extracted from your home screen wallpaper</string>
<string name="settings_appearance_use_system_font_title">Use system font</string>
<string name="settings_application_language">Application Language</string> <string name="settings_application_language">Application Language</string>
<string name="settings_application_language_value">Current: %1$s</string> <string name="settings_application_language_value">Current: %1$s</string>
@@ -237,9 +235,6 @@
<string name="settings_general_title">General</string> <string name="settings_general_title">General</string>
<string name="settings_general_contact_names_title">Use contact names</string> <string name="settings_general_contact_names_title">Use contact names</string>
<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_summary">Show emoji button in chat panel</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>
<string name="settings_appearance_multiline_summary">The title of the conversation and the text of the message can take up multiple lines</string> <string name="settings_appearance_multiline_summary">The title of the conversation and the text of the message can take up multiple lines</string>
@@ -247,18 +242,9 @@
<string name="settings_features_fast_text_title">Fast text</string> <string name="settings_features_fast_text_title">Fast text</string>
<string name="settings_features_long_poll_in_background_title">LongPoll in background</string> <string name="settings_features_long_poll_in_background_title">LongPoll in background</string>
<string name="settings_features_long_poll_in_background_summary">Your messages will be updating even when app is not on the screen</string> <string name="settings_features_long_poll_in_background_summary">Your messages will be updating even when app is not on the screen</string>
<string name="settings_features_show_time_in_action_messages_title">Show time in action messages</string>
<string name="settings_experimental_use_blur_title">Use blur</string>
<string name="settings_experimental_use_blur_summary">Adds blur wherever possible.\nWorks on android 12 and newer</string>
<string name="settings_experimental_more_animations_title">More animations</string>
<string name="settings_experimental_more_animations_summary">Use animations wherever possible</string>
<string name="settings_activity_title">Activity</string> <string name="settings_activity_title">Activity</string>
<string name="settings_activity_send_online_title">Send online status</string> <string name="settings_activity_send_online_title">Send online status</string>
<string name="settings_activity_send_online_summary">Online status will be sent every five minutes</string> <string name="settings_activity_send_online_summary">Online status will be sent every five minutes</string>
<string name="settings_experimental_title">Experimental - VERY unstable</string>
<string name="settings_debug_title">Debug</string> <string name="settings_debug_title">Debug</string>
<string name="background_long_poll_rationale_text">The app won\'t be able to update messages in the background without access to notifications</string> <string name="background_long_poll_rationale_text">The app won\'t be able to update messages in the background without access to notifications</string>
<string name="action_disable">Disable</string> <string name="action_disable">Disable</string>
@@ -345,13 +345,6 @@ class LoginViewModelImpl(
true true
} }
is State.Error.TestError -> {
val message = stateError.message
val error = LoginError.SimpleError(message = message)
loginError.update { error }
true
}
else -> false else -> false
} }
} }
@@ -9,5 +9,4 @@ sealed class LoginError {
data object TooManyTries : LoginError() data object TooManyTries : LoginError()
data object WrongValidationCode : LoginError() data object WrongValidationCode : LoginError()
data object WrongValidationCodeFormat : LoginError() data object WrongValidationCodeFormat : LoginError()
data class SimpleError(val message: String): LoginError()
} }
@@ -50,6 +50,8 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.meloda.fast.auth.login.LoginViewModel
import dev.meloda.fast.auth.login.LoginViewModelImpl
import dev.meloda.fast.auth.login.model.CaptchaArguments import dev.meloda.fast.auth.login.model.CaptchaArguments
import dev.meloda.fast.auth.login.model.LoginError import dev.meloda.fast.auth.login.model.LoginError
import dev.meloda.fast.auth.login.model.LoginScreenState import dev.meloda.fast.auth.login.model.LoginScreenState
@@ -439,14 +441,5 @@ fun HandleError(
confirmText = stringResource(id = UiR.string.ok) confirmText = stringResource(id = UiR.string.ok)
) )
} }
is LoginError.SimpleError -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = error.message,
confirmText = stringResource(id = UiR.string.ok)
)
}
} }
} }
@@ -62,8 +62,8 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel
@@ -137,7 +137,7 @@ fun ChatMaterialsScreen(
) )
} }
val titles = listOf("Photos", "Videos", "Audios")//, "Files", "Links") val titles = listOf("Photos", "Videos", "Audios", "Files", "Links")
val listState = rememberLazyListState() val listState = rememberLazyListState()
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
@@ -179,7 +179,7 @@ fun ChatMaterialsScreen(
modifier = Modifier modifier = Modifier
.then( .then(
if (currentTheme.enableBlur) { if (currentTheme.enableBlur) {
Modifier.hazeEffect( Modifier.hazeChild(
state = hazeState, state = hazeState,
style = hazeStyle style = hazeStyle
) )
@@ -311,7 +311,7 @@ fun ChatMaterialsScreen(
modifier = Modifier modifier = Modifier
.then( .then(
if (currentTheme.enableBlur) { if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState) Modifier.haze(state = hazeState)
} else { } else {
Modifier Modifier
} }
@@ -346,7 +346,7 @@ fun ChatMaterialsScreen(
modifier = Modifier modifier = Modifier
.then( .then(
if (currentTheme.enableBlur) { if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState) Modifier.haze(state = hazeState)
} else { } else {
Modifier Modifier
} }
@@ -1,11 +1,8 @@
package dev.meloda.fast.conversations package dev.meloda.fast.conversations
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
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 dev.meloda.fast.common.extensions.createTimerFlow import dev.meloda.fast.common.extensions.createTimerFlow
import dev.meloda.fast.common.extensions.findWithIndex import dev.meloda.fast.common.extensions.findWithIndex
@@ -21,7 +18,6 @@ import dev.meloda.fast.data.State
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.ConversationsUseCase import dev.meloda.fast.domain.ConversationsUseCase
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
@@ -47,6 +43,7 @@ interface ConversationsViewModel {
val screenState: StateFlow<ConversationsScreenState> val screenState: StateFlow<ConversationsScreenState>
val baseError: StateFlow<BaseError?> val baseError: StateFlow<BaseError?>
val imagesToPreload: StateFlow<List<String>>
val currentOffset: StateFlow<Int> val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean> val canPaginate: StateFlow<Boolean>
val scrollToTop: StateFlow<Boolean> val scrollToTop: StateFlow<Boolean>
@@ -81,23 +78,16 @@ class ConversationsViewModelImpl(
private val conversationsUseCase: ConversationsUseCase, private val conversationsUseCase: ConversationsUseCase,
private val messagesUseCase: MessagesUseCase, private val messagesUseCase: MessagesUseCase,
private val resources: Resources, private val resources: Resources,
private val userSettings: UserSettings, private val userSettings: UserSettings
private val imageLoader: ImageLoader,
private val applicationContext: Context,
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
) : ConversationsViewModel, ViewModel() { ) : ConversationsViewModel, ViewModel() {
override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY) override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
override val baseError = MutableStateFlow<BaseError?>(null) override val baseError = MutableStateFlow<BaseError?>(null)
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
override val currentOffset = MutableStateFlow(0) override val currentOffset = MutableStateFlow(0)
override val canPaginate = MutableStateFlow(false) override val canPaginate = MutableStateFlow(false)
override val scrollToTop = MutableStateFlow(false) override val scrollToTop = MutableStateFlow(false)
// TODO: 22-Dec-24, Danil Nikolaev: rewrite
private val useContactNames = {
userSettings.useContactNames.value
}
override fun onPaginationConditionsMet() { override fun onPaginationConditionsMet() {
currentOffset.update { screenState.value.conversations.size } currentOffset.update { screenState.value.conversations.size }
loadConversations() loadConversations()
@@ -134,7 +124,6 @@ class ConversationsViewModelImpl(
} }
override fun onRefresh() { override fun onRefresh() {
baseError.setValue { null }
loadConversations(offset = 0) loadConversations(offset = 0)
} }
@@ -274,7 +263,17 @@ class ConversationsViewModelImpl(
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset) conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset)
.listenValue(viewModelScope) { state -> .listenValue(viewModelScope) { state ->
state.processState( state.processState(
error = ::handleError, error = { error ->
if (error is State.Error.ApiError) {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> Unit
}
}
},
success = { response -> success = { response ->
val itemsCountSufficient = response.size == LOAD_COUNT val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient } canPaginate.setValue { itemsCountSufficient }
@@ -282,17 +281,9 @@ class ConversationsViewModelImpl(
val paginationExhausted = !itemsCountSufficient && val paginationExhausted = !itemsCountSufficient &&
screenState.value.conversations.isNotEmpty() screenState.value.conversations.isNotEmpty()
val imagesToPreload = imagesToPreload.setValue {
response.mapNotNull { it.extractAvatar().extractUrl() } response.mapNotNull { it.extractAvatar().extractUrl() }
imagesToPreload.forEach { url ->
imageLoader.enqueue(
ImageRequest.Builder(applicationContext)
.data(url)
.build()
)
} }
conversationsUseCase.storeConversations(response) conversationsUseCase.storeConversations(response)
val loadedConversations = response.map { val loadedConversations = response.map {
@@ -330,40 +321,6 @@ class ConversationsViewModelImpl(
} }
} }
private fun handleError(error: State.Error) {
when (error) {
is State.Error.ApiError -> {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> {
baseError.setValue {
BaseError.SimpleError(message = error.errorMessage)
}
}
}
}
State.Error.ConnectionError -> {
baseError.setValue {
BaseError.SimpleError(message = "Connection error")
}
}
State.Error.InternalError -> {
baseError.setValue {
BaseError.SimpleError(message = "Internal error")
}
}
State.Error.UnknownError -> {
baseError.setValue {
BaseError.SimpleError(message = "Unknown error")
}
}
else -> Unit
}
}
private fun deleteConversation(peerId: Int) { private fun deleteConversation(peerId: Int) {
conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state -> conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state ->
state.processState( state.processState(
@@ -380,12 +337,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations } conversations.update { newConversations }
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
) )
} }
} }
@@ -421,40 +373,13 @@ class ConversationsViewModelImpl(
private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
val message = event.message val message = event.message
val newConversations = conversations.value.toMutableList() val newConversations = conversations.value.toMutableList()
val conversationIndex = val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == message.peerId } newConversations.indexOfFirstOrNull { it.id == message.peerId }
if (conversationIndex == null) { if (conversationIndex == null) { // диалога нет в списке
loadConversationsByIdUseCase(peerIds = listOf(message.peerId)) // pizdets
.listenValue(viewModelScope) { state -> // TODO: 04/07/2024, Danil Nikolaev: load conversation and store info
state.processState(
error = { error ->
},
success = { response ->
val conversation = (response.firstOrNull() ?: return@listenValue)
.copy(lastMessage = message)
// TODO: 22-Dec-24, Danil Nikolaev: handle interactions and pinned state
newConversations.add(pinnedConversationsCount.value, conversation)
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
)
}
} else { } else {
val conversation = newConversations[conversationIndex] val conversation = newConversations[conversationIndex]
var newConversation = conversation.copy( var newConversation = conversation.copy(
@@ -495,12 +420,7 @@ class ConversationsViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
) )
} }
} }
@@ -524,12 +444,7 @@ class ConversationsViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
) )
} }
} }
@@ -539,11 +454,8 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList() val newConversations = conversations.value.toMutableList()
val conversationIndex = val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId } newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] = newConversations[conversationIndex] =
newConversations[conversationIndex].copy( newConversations[conversationIndex].copy(
inRead = event.messageId, inRead = event.messageId,
@@ -554,26 +466,17 @@ class ConversationsViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
) )
} }
)
}
}
} }
private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) { private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) {
val newConversations = conversations.value.toMutableList() val newConversations = conversations.value.toMutableList()
val conversationIndex = val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId } newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] = newConversations[conversationIndex] =
newConversations[conversationIndex].copy( newConversations[conversationIndex].copy(
outRead = event.messageId, outRead = event.messageId,
@@ -583,15 +486,9 @@ class ConversationsViewModelImpl(
conversations.update { newConversations } conversations.update { newConversations }
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
) )
} }
)
}
}
} }
private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) { private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) {
@@ -599,11 +496,8 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList() val newConversations = conversations.value.toMutableList()
val conversationIndex = val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId } newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
val pin = event.majorId > 0 val pin = event.majorId > 0
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId) val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
@@ -629,13 +523,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations } conversations.update { newConversations }
screenState.setValue { old -> screenState.setValue { old ->
old.copy(conversations = newConversations.map { old.copy(conversations = newConversations.map { it.asPresentation(resources) })
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
})
}
} }
} }
@@ -655,11 +543,8 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList() val newConversations = conversations.value.toMutableList()
val conversationAndIndex = val conversationAndIndex =
newConversations.findWithIndex { it.id == peerId } newConversations.findWithIndex { it.id == peerId } ?: return
if (conversationAndIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationAndIndex.first] = newConversations[conversationAndIndex.first] =
conversationAndIndex.second.copy( conversationAndIndex.second.copy(
interactionType = interactionType.value, interactionType = interactionType.value,
@@ -670,12 +555,7 @@ class ConversationsViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
) )
} }
@@ -703,7 +583,6 @@ class ConversationsViewModelImpl(
stopInteraction(peerId, newInteractionJob) stopInteraction(peerId, newInteractionJob)
} }
} }
}
private fun stopInteraction(peerId: Int, interactionJob: InteractionJob) { private fun stopInteraction(peerId: Int, interactionJob: InteractionJob) {
interactionsTimers[peerId] ?: return interactionsTimers[peerId] ?: return
@@ -721,12 +600,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations } conversations.update { newConversations }
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
) )
} }
@@ -755,12 +629,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations } conversations.update { newConversations }
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
conversations = newConversations.map { conversations = newConversations.map { it.asPresentation(resources) }
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
) )
} }
} }
@@ -54,6 +54,7 @@ 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
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -64,6 +65,8 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.HapticFeedbackConstantsCompat import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
import coil.request.ImageRequest
import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
@@ -93,11 +96,24 @@ fun ConversationsRoute(
onConversationPhotoClicked: (url: String) -> Unit, onConversationPhotoClicked: (url: String) -> Unit,
viewModel: ConversationsViewModel viewModel: ConversationsViewModel
) { ) {
val context = LocalContext.current
val screenState by viewModel.screenState.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle() val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
LaunchedEffect(imagesToPreload) {
imagesToPreload.forEach { url ->
context.imageLoader.enqueue(
ImageRequest.Builder(context)
.data(url)
.build()
)
}
}
ConversationsScreen( ConversationsScreen(
screenState = screenState, screenState = screenState,
baseError = baseError, baseError = baseError,
@@ -339,9 +355,7 @@ fun ConversationsScreen(
} }
) { padding -> ) { padding ->
when { when {
baseError != null -> { baseError is BaseError.SessionExpired -> {
when (baseError) {
is BaseError.SessionExpired -> {
ErrorView( ErrorView(
text = "Session expired", text = "Session expired",
buttonText = "Log out", buttonText = "Log out",
@@ -349,16 +363,6 @@ fun ConversationsScreen(
) )
} }
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = "Try again",
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader() screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
else -> { else -> {
@@ -33,7 +33,7 @@ import dev.meloda.fast.ui.R as UiR
fun VkConversation.asPresentation( fun VkConversation.asPresentation(
resources: Resources, resources: Resources,
useContactName: Boolean useContactName: Boolean = false
): UiConversation = UiConversation( ): UiConversation = UiConversation(
id = id, id = id,
lastMessageId = lastMessageId, lastMessageId = lastMessageId,
@@ -278,14 +278,14 @@ class SettingsViewModelImpl(
) )
val generalShowEmojiButton = SettingsItem.Switch( val generalShowEmojiButton = SettingsItem.Switch(
key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON, key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
title = UiText.Resource(UiR.string.settings_general_show_emoji_button_title), title = UiText.Simple("Show emoji button"),
text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary), text = UiText.Simple("Show emoji button in chat panel"),
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_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,
title = UiText.Resource(UiR.string.settings_general_enable_haptic_title) title = UiText.Simple("Enable haptic")
) )
val appearanceTitle = SettingsItem.Title( val appearanceTitle = SettingsItem.Title(
@@ -342,7 +342,7 @@ class SettingsViewModelImpl(
val appearanceUseSystemFont = SettingsItem.Switch( val appearanceUseSystemFont = SettingsItem.Switch(
key = SettingsKeys.KEY_USE_SYSTEM_FONT, key = SettingsKeys.KEY_USE_SYSTEM_FONT,
defaultValue = SettingsKeys.DEFAULT_USE_SYSTEM_FONT, defaultValue = SettingsKeys.DEFAULT_USE_SYSTEM_FONT,
title = UiText.Resource(UiR.string.settings_appearance_use_system_font_title) title = UiText.Simple("Use system font")
) )
val appearanceLanguage = SettingsItem.TitleText( val appearanceLanguage = SettingsItem.TitleText(
key = SettingsKeys.KEY_APPEARANCE_LANGUAGE, key = SettingsKeys.KEY_APPEARANCE_LANGUAGE,
@@ -379,7 +379,7 @@ class SettingsViewModelImpl(
val experimentalTitle = SettingsItem.Title( val experimentalTitle = SettingsItem.Title(
key = "experimental", key = "experimental",
title = UiText.Resource(UiR.string.settings_experimental_title) title = UiText.Simple("Experimental - VERY unstable")
) )
val experimentalLongPollBackground = SettingsItem.Switch( val experimentalLongPollBackground = SettingsItem.Switch(
key = SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND, key = SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND,
@@ -390,20 +390,19 @@ class SettingsViewModelImpl(
val experimentalShowTimeInActionMessages = SettingsItem.Switch( val experimentalShowTimeInActionMessages = SettingsItem.Switch(
key = SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES, key = SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES,
defaultValue = SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES, defaultValue = SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES,
title = UiText.Resource(UiR.string.settings_features_show_time_in_action_messages_title) title = UiText.Simple("Show time in action messages")
) )
val experimentalUseBlur = SettingsItem.Switch( val experimentalUseBlur = SettingsItem.Switch(
key = SettingsKeys.KEY_USE_BLUR, key = SettingsKeys.KEY_USE_BLUR,
defaultValue = SettingsKeys.DEFAULT_USE_BLUR, defaultValue = SettingsKeys.DEFAULT_USE_BLUR,
title = UiText.Resource(UiR.string.settings_experimental_use_blur_title), title = UiText.Simple("Use blur"),
text = UiText.Resource(UiR.string.settings_experimental_use_blur_summary), text = UiText.Simple("Adds blur wherever possible\nWorks on android 12 and newer"),
isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
) )
val enableAnimations = SettingsItem.Switch( val enableAnimations = SettingsItem.Switch(
key = SettingsKeys.KEY_MORE_ANIMATIONS, key = SettingsKeys.KEY_MORE_ANIMATIONS,
defaultValue = SettingsKeys.DEFAULT_MORE_ANIMATIONS, defaultValue = SettingsKeys.DEFAULT_MORE_ANIMATIONS,
title = UiText.Resource(UiR.string.settings_experimental_more_animations_title), title = UiText.Simple("More animations"),
text = UiText.Resource(UiR.string.settings_experimental_more_animations_summary) text = UiText.Simple("Use animations wherever possible")
) )
val debugTitle = SettingsItem.Title( val debugTitle = SettingsItem.Title(
+14 -14
View File
@@ -2,25 +2,25 @@
minSdk = "23" minSdk = "23"
targetSdk = "35" targetSdk = "35"
compileSdk = "35" compileSdk = "35"
versionCode = "9" versionCode = "8"
versionName = "0.1.6" versionName = "0.1.5"
agp = "8.9.0" agp = "8.7.3"
converterMoshi = "2.11.0" converterMoshi = "2.11.0"
eithernet = "2.0.0" eithernet = "2.0.0"
haze = "1.5.1" haze = "1.1.1"
kotlin = "2.1.10" kotlin = "2.1.0"
ksp = "2.1.10-1.0.31" ksp = "2.1.0-1.0.29"
compose-bom = "2025.03.00" compose-bom = "2024.12.01"
koin = "4.0.2" koin = "4.0.0"
accompanist = "0.37.2" accompanist = "0.37.0"
coil = "2.7.0" coil = "2.7.0"
coroutines = "1.10.1" coroutines = "1.9.0"
junit = "4.13.2" junit = "4.13.2"
chucker = "4.1.0" chucker = "4.1.0"
guava = "33.4.5-jre" guava = "33.3.1-jre"
lifecycle = "2.8.7" lifecycle = "2.8.7"
core-ktx = "1.15.0" core-ktx = "1.15.0"
material = "1.12.0" material = "1.12.0"
@@ -33,10 +33,10 @@ nanokt = "1.2.0"
junitVersion = "1.2.1" junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
appcompat = "1.7.0" appcompat = "1.7.0"
androidx-navigation = "2.8.9" androidx-navigation = "2.8.5"
serialization = "1.8.0" serialization = "1.7.3"
rebugger = "1.0.0-rc03" rebugger = "1.0.0-rc03"
moduleGraph = "2.8.0" moduleGraph = "2.7.1"
[libraries] [libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
Binary file not shown.
+2 -3
View File
@@ -1,7 +1,6 @@
#Mon Oct 28 18:41:43 MSK 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+13 -31
View File
@@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -82,12 +80,13 @@ do
esac esac
done done
# This is normally unused APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
' "$PWD" ) || exit DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -134,29 +133,22 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -201,15 +193,11 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# Collect all arguments for the java command: # * put everything else in single quotes, so that it's not re-expanded.
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -217,12 +205,6 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+16 -21
View File
@@ -13,10 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@@ -27,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -43,13 +40,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if "%ERRORLEVEL%" == "0" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@@ -59,11 +56,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@@ -78,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd if "%ERRORLEVEL%"=="0" goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL% if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
if %EXIT_CODE% equ 0 set EXIT_CODE=1 exit /b 1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal