Upstream changes (#23)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,57 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize)
|
||||
alias(libs.plugins.kotlin.compose.compiler)
|
||||
}
|
||||
|
||||
group = "com.meloda.app.fast.common"
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.common"
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Configs.minSdk
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = Configs.java
|
||||
targetCompatibility = Configs.java
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Configs.java.toString()
|
||||
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.preference.ktx)
|
||||
|
||||
implementation(libs.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.lifecycle.runtime.ktx)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
implementation(libs.koin.androidx.compose.navigation)
|
||||
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
implementation(libs.nanokt.jvm)
|
||||
implementation(libs.nanokt.android)
|
||||
implementation(libs.nanokt)
|
||||
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
implementation(libs.kotlin.serialization)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
object AppConstants {
|
||||
|
||||
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
|
||||
|
||||
const val API_VERSION = "5.173"
|
||||
const val URL_OAUTH = "https://oauth.vk.com"
|
||||
const val URL_API = "https://api.vk.com/method"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.net.URLEncoder
|
||||
|
||||
class AuthInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val builder = chain.request().url.newBuilder()
|
||||
|
||||
val uri = builder.build().toUri().toString().toUri()
|
||||
|
||||
if (uri.getQueryParameter("v") == null) {
|
||||
builder.addQueryParameter(
|
||||
name = "v",
|
||||
value = URLEncoder.encode(AppConstants.API_VERSION, "utf-8")
|
||||
)
|
||||
}
|
||||
|
||||
if (UserConfig.accessToken.isNotBlank()) {
|
||||
builder.addQueryParameter(
|
||||
"access_token",
|
||||
URLEncoder.encode(UserConfig.accessToken, "utf-8")
|
||||
)
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.navigation.NavType
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
inline fun <reified T : Parcelable> customNavType(
|
||||
isNullableAllowed: Boolean = false,
|
||||
json: Json = Json
|
||||
) = object : NavType<T>(isNullableAllowed = isNullableAllowed) {
|
||||
override fun get(bundle: Bundle, key: String) =
|
||||
BundleCompat.getParcelable(bundle, key, T::class.java)
|
||||
|
||||
override fun parseValue(value: String): T = json.decodeFromString(value)
|
||||
|
||||
override fun serializeAsValue(value: T): String = json.encodeToString(value)
|
||||
|
||||
override fun put(bundle: Bundle, key: String, value: T) = bundle.putParcelable(key, value)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
sealed class UiImage {
|
||||
|
||||
data class Resource(@DrawableRes val resId: Int) : UiImage()
|
||||
|
||||
data class Simple(val drawable: Drawable) : UiImage()
|
||||
|
||||
data class Color(@ColorInt val color: Int) : UiImage()
|
||||
|
||||
data class ColorResource(@ColorRes val resId: Int) : UiImage()
|
||||
|
||||
data class Url(val url: String) : UiImage()
|
||||
|
||||
fun extractUrl(): String? = when (this) {
|
||||
is Url -> this.url
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun extractResId(): Int = when (this) {
|
||||
is Resource -> this.resId
|
||||
else -> throw IllegalStateException("this UiImage is not Resource")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
sealed class UiText {
|
||||
|
||||
data object Empty : UiText()
|
||||
|
||||
data class Resource(@StringRes val resId: Int) : UiText()
|
||||
|
||||
data class ResourceParams(
|
||||
@StringRes val value: Int,
|
||||
val args: List<Any?>,
|
||||
) : UiText()
|
||||
|
||||
data class Simple(val text: String) : UiText()
|
||||
|
||||
data class QuantityResource(@PluralsRes val resId: Int, val quantity: Int) : UiText()
|
||||
}
|
||||
|
||||
fun UiText?.parseString(resources: Resources): String? {
|
||||
return when (this) {
|
||||
is UiText.Resource -> resources.getString(resId)
|
||||
is UiText.ResourceParams -> {
|
||||
val processedArgs = args.map { any ->
|
||||
when (any) {
|
||||
is UiText -> any.parseString(resources)
|
||||
else -> any
|
||||
}
|
||||
}
|
||||
resources.getString(value, *processedArgs.toTypedArray())
|
||||
}
|
||||
|
||||
is UiText.QuantityResource -> resources.getQuantityString(resId, quantity, quantity)
|
||||
is UiText.Simple -> text
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
object UserConfig {
|
||||
|
||||
private const val ARG_CURRENT_USER_ID = "current_user_id"
|
||||
|
||||
private var preferences: SharedPreferences by Delegates.notNull()
|
||||
|
||||
fun init(preferences: SharedPreferences) {
|
||||
this.preferences = preferences
|
||||
}
|
||||
|
||||
var currentUserId: Int = -1
|
||||
get() = preferences.getInt(ARG_CURRENT_USER_ID, -1)
|
||||
set(value) {
|
||||
field = value
|
||||
preferences.edit { putInt(ARG_CURRENT_USER_ID, value) }
|
||||
}
|
||||
|
||||
var userId: Int = -1
|
||||
var accessToken: String = ""
|
||||
var fastToken: String? = ""
|
||||
var trustedHash: String? = null
|
||||
|
||||
fun clear() {
|
||||
currentUserId = -1
|
||||
accessToken = ""
|
||||
fastToken = ""
|
||||
userId = -1
|
||||
}
|
||||
|
||||
fun isLoggedIn(): Boolean {
|
||||
return currentUserId > 0 && userId > 0 && accessToken.isNotBlank()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
object VkConstants {
|
||||
|
||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||
|
||||
const val USER_FIELDS =
|
||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate"
|
||||
|
||||
const val ALL_FIELDS =
|
||||
"$USER_FIELDS,$GROUP_FIELDS"
|
||||
|
||||
const val LP_VERSION = 10
|
||||
|
||||
const val VK_APP_ID = "2274003"
|
||||
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
||||
|
||||
const val FAST_GROUP_ID = -119516304
|
||||
const val FAST_APP_ID = "6964679"
|
||||
|
||||
object Auth {
|
||||
const val SCOPE = "notify," +
|
||||
"friends," +
|
||||
"photos," +
|
||||
"audio," +
|
||||
"video," +
|
||||
"docs," +
|
||||
"status," +
|
||||
"notes," +
|
||||
"pages," +
|
||||
"wall," +
|
||||
"groups," +
|
||||
"messages," +
|
||||
"offline," +
|
||||
"notifications"
|
||||
|
||||
object GrantType {
|
||||
const val PASSWORD = "password"
|
||||
}
|
||||
}
|
||||
|
||||
// val restrictedToEditAttachments = listOf<Class<out VkAttachment>>(
|
||||
// VkCallDomain::class.java,
|
||||
// VkCuratorDomain::class.java,
|
||||
// VkEventDomain::class.java,
|
||||
// VkGiftDomain::class.java,
|
||||
// VkGraffitiDomain::class.java,
|
||||
// VkGroupCallDomain::class.java,
|
||||
// VkStoryDomain::class.java,
|
||||
// VkAudioMessageDomain::class.java,
|
||||
// VkWidgetDomain::class.java
|
||||
// )
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.meloda.app.fast.common.di
|
||||
|
||||
import coil.ImageLoader
|
||||
import org.koin.dsl.module
|
||||
|
||||
val commonModule = module {
|
||||
single {
|
||||
ImageLoader.Builder(get())
|
||||
.crossfade(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.meloda.app.fast.common.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
fun Context.restartApp() {
|
||||
(this as? Activity)?.let { activity ->
|
||||
activity.finishAffinity()
|
||||
activity.startActivity(
|
||||
Intent(
|
||||
this,
|
||||
Class.forName("com.meloda.app.fast.MainActivity")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> Iterable<T>.findWithIndex(predicate: (T) -> Boolean): Pair<Int, T>? {
|
||||
val value = firstOrNull(predicate) ?: return null
|
||||
return indexOf(value).let { index -> if (index == -1) null else index to value }
|
||||
}
|
||||
|
||||
fun <T> MutableList<T>.addIf(element: T, condition: () -> Boolean) {
|
||||
if (condition.invoke()) add(element)
|
||||
}
|
||||
|
||||
context(ViewModel)
|
||||
fun <T> Flow<T>.listenValue(action: suspend (T) -> Unit) = listenValue(viewModelScope, action)
|
||||
|
||||
fun <T> Flow<T>.listenValue(
|
||||
coroutineScope: CoroutineScope,
|
||||
action: suspend (T) -> Unit
|
||||
): Job = onEach(action::invoke).launchIn(coroutineScope)
|
||||
|
||||
fun createTimerFlow(
|
||||
time: Int,
|
||||
onStartAction: (suspend () -> Unit)? = null,
|
||||
onTickAction: (suspend (remainedTime: Int) -> Unit)? = null,
|
||||
onTimeoutAction: (suspend () -> Unit)? = null,
|
||||
interval: Duration = 1.seconds
|
||||
): Flow<Int> = (time downTo 0)
|
||||
.asSequence()
|
||||
.asFlow()
|
||||
.onStart { onStartAction?.invoke() }
|
||||
.onEach { timeLeft ->
|
||||
onTickAction?.invoke(timeLeft)
|
||||
if (timeLeft == 0) {
|
||||
onTimeoutAction?.invoke()
|
||||
} else {
|
||||
delay(interval)
|
||||
}
|
||||
}
|
||||
|
||||
fun createTimerFlow(
|
||||
isNeedToEndCondition: suspend () -> Boolean,
|
||||
onStartAction: (suspend () -> Unit)? = null,
|
||||
onTickAction: (suspend () -> Unit)? = null,
|
||||
onEndAction: (suspend () -> Unit)? = null,
|
||||
interval: Duration = 1.seconds
|
||||
): Flow<Boolean> = flow {
|
||||
while (true) {
|
||||
val isNeedToEnd = isNeedToEndCondition()
|
||||
emit(isNeedToEnd)
|
||||
if (isNeedToEnd) break
|
||||
}
|
||||
}
|
||||
.onStart { onStartAction?.invoke() }
|
||||
.onEach { isNeedToEnd ->
|
||||
onTickAction?.invoke()
|
||||
if (isNeedToEnd) {
|
||||
onEndAction?.invoke()
|
||||
} else {
|
||||
delay(interval)
|
||||
}
|
||||
}
|
||||
|
||||
context(ViewModel)
|
||||
fun <T> MutableSharedFlow<T>.emitOnMainScope(value: T) = emitOnScope(Dispatchers.Main) { value }
|
||||
|
||||
context(ViewModel)
|
||||
fun <T> MutableSharedFlow<T>.emitOnScope(
|
||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
value: () -> T,
|
||||
) {
|
||||
viewModelScope.launch(coroutineContext) {
|
||||
emit(value())
|
||||
}
|
||||
}
|
||||
|
||||
context(CoroutineScope)
|
||||
suspend fun <T> MutableSharedFlow<T>.emitWithMain(value: T) {
|
||||
withContext(Dispatchers.Main) {
|
||||
emit(value)
|
||||
}
|
||||
}
|
||||
|
||||
context(ViewModel)
|
||||
fun <T> MutableStateFlow<T>.updateValue(newValue: T) = this.update { newValue }
|
||||
|
||||
fun <T> MutableStateFlow<T>.setValue(function: (T) -> T) {
|
||||
val newValue = function(value)
|
||||
update { newValue }
|
||||
}
|
||||
|
||||
fun Any.asInt(): Int {
|
||||
return when (this) {
|
||||
is Number -> this.toInt()
|
||||
|
||||
else -> throw IllegalArgumentException("Object is not numeric")
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
|
||||
return when (this) {
|
||||
is List<*> -> this.mapNotNull { it?.run(mapper) }
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= sdkInt) {
|
||||
action?.invoke()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.meloda.app.fast.common.extensions
|
||||
|
||||
inline fun String?.ifEmpty(defaultValue: () -> String?): String? =
|
||||
if (this?.isEmpty() == true) defaultValue() else this
|
||||
|
||||
fun String?.orDots(count: Int = 3): String {
|
||||
return this ?: ("." * count)
|
||||
}
|
||||
|
||||
operator fun String.times(count: Int): String {
|
||||
val builder = StringBuilder()
|
||||
for (i in 0 until count) {
|
||||
builder.append(this)
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.meloda.app.fast.common.extensions.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||
|
||||
@Composable
|
||||
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(navController: NavController): T {
|
||||
val navGraphRoute = destination.parent?.route ?: return koinViewModel()
|
||||
val parentEntry = remember(this) {
|
||||
navController.getBackStackEntry(navGraphRoute)
|
||||
}
|
||||
return koinNavViewModel(viewModelStoreOwner = parentEntry)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.meloda.app.fast.common.util
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
private object BuildConfig {
|
||||
const val DEBUG = true
|
||||
const val APPLICATION_ID = "com.meloda.app.fast"
|
||||
}
|
||||
|
||||
object AndroidUtils {
|
||||
|
||||
fun copyText(
|
||||
context: Context,
|
||||
label: String? = "",
|
||||
text: String,
|
||||
withToast: Boolean = false
|
||||
) {
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text))
|
||||
|
||||
if (withToast && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun copyImage(
|
||||
context: Context,
|
||||
label: String? = "",
|
||||
imageUri: Uri,
|
||||
withToast: Boolean = false
|
||||
) {
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
clipboardManager.setPrimaryClip(ClipData.newRawUri(label, imageUri))
|
||||
|
||||
if (withToast && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun bytesToMegabytes(bytes: Double): Double {
|
||||
return bytes / 1024 / 1024
|
||||
}
|
||||
|
||||
fun bytesToHumanReadableSize(bytes: Double): String = when {
|
||||
bytes >= 1 shl 30 -> "%.1f GB".format(bytes / (1 shl 30))
|
||||
bytes >= 1 shl 20 -> "%.1f MB".format(bytes / (1 shl 20))
|
||||
bytes >= 1 shl 10 -> "%.1f KB".format(bytes / (1 shl 10))
|
||||
else -> "$bytes B"
|
||||
}
|
||||
|
||||
fun openAppNotificationsSettings(context: Context) {
|
||||
val packageName = context.packageName
|
||||
|
||||
val intent = Intent("android.settings.APP_NOTIFICATION_SETTINGS")
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
intent.putExtra("android.provider.extra.APP_PACKAGE", packageName)
|
||||
} else {
|
||||
intent.putExtra("app_package", packageName)
|
||||
intent.putExtra("app_uid", context.applicationInfo.uid)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun isCanInstallUnknownApps(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
Settings.Secure.getInt(
|
||||
context.contentResolver,
|
||||
Settings.Secure.INSTALL_NON_MARKET_APPS
|
||||
) == 1
|
||||
} else {
|
||||
context.packageManager.canRequestPackageInstalls()
|
||||
}
|
||||
}
|
||||
|
||||
fun openInstallUnknownAppsScreen(context: Context) {
|
||||
context.startActivity(Intent().apply {
|
||||
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
Settings.ACTION_SECURITY_SETTINGS
|
||||
} else {
|
||||
data = Uri.parse("package:${BuildConfig.APPLICATION_ID}")
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun getInstallPackageIntent(
|
||||
context: Context,
|
||||
providerPath: String,
|
||||
fileToRead: File,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||
intent.data = FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + providerPath,
|
||||
fileToRead
|
||||
)
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
fun isBatterySaverOn(context: Context): Boolean {
|
||||
return (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode == true
|
||||
}
|
||||
|
||||
fun getImageToShare(context: Context, existingFile: File): Uri? {
|
||||
val imageFolder = File(context.cacheDir, "images")
|
||||
|
||||
return try {
|
||||
imageFolder.mkdirs()
|
||||
|
||||
val copyToFile = File(imageFolder, "shared_image.png")
|
||||
if (copyToFile.exists()) {
|
||||
copyToFile.delete()
|
||||
}
|
||||
|
||||
val file = existingFile.copyTo(copyToFile)
|
||||
FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", file)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getImageToShare(context: Context, bitmap: Bitmap): Uri? {
|
||||
val imageFolder = File(context.cacheDir, "images")
|
||||
|
||||
return try {
|
||||
imageFolder.mkdirs()
|
||||
|
||||
val file = File(imageFolder, "shared_image.png")
|
||||
val outputStream = FileOutputStream(file)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, outputStream)
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun showShareSheet(context: Context, content: ShareContent) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
|
||||
type = when (content) {
|
||||
is ShareContent.Text -> {
|
||||
putExtra(Intent.EXTRA_TEXT, content.text)
|
||||
"text/plain"
|
||||
}
|
||||
|
||||
is ShareContent.Image -> {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
putExtra(Intent.EXTRA_STREAM, content.uri)
|
||||
"image/png"
|
||||
}
|
||||
|
||||
is ShareContent.TextWithImage -> {
|
||||
putExtra(Intent.EXTRA_TEXT, content.text)
|
||||
putExtra(Intent.EXTRA_STREAM, content.imageUri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
"image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val contentType = when (content) {
|
||||
is ShareContent.Text -> "Text"
|
||||
is ShareContent.Image -> "Image"
|
||||
is ShareContent.TextWithImage -> "Text with image"
|
||||
}
|
||||
val chooserIntent = Intent.createChooser(intent, "Share $contentType")
|
||||
|
||||
|
||||
context.startActivity(chooserIntent)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ShareContent {
|
||||
data class Text(val text: String) : ShareContent()
|
||||
|
||||
data class Image(val uri: Uri) : ShareContent()
|
||||
|
||||
data class TextWithImage(val text: String, val imageUri: Uri) : ShareContent()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.meloda.app.fast.common.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import com.conena.nanokt.jvm.util.dayOfMonth
|
||||
import com.conena.nanokt.jvm.util.hour
|
||||
import com.conena.nanokt.jvm.util.hourOfDay
|
||||
import com.conena.nanokt.jvm.util.millisecond
|
||||
import com.conena.nanokt.jvm.util.minute
|
||||
import com.conena.nanokt.jvm.util.month
|
||||
import com.conena.nanokt.jvm.util.second
|
||||
import com.conena.nanokt.jvm.util.year
|
||||
import com.meloda.app.fast.common.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object TimeUtils {
|
||||
|
||||
fun removeTime(date: Date): Long {
|
||||
return Calendar.getInstance().apply {
|
||||
time = date
|
||||
hourOfDay = 0
|
||||
minute = 0
|
||||
second = 0
|
||||
millisecond = 0
|
||||
}.timeInMillis
|
||||
}
|
||||
|
||||
fun getLocalizedDate(resources: Resources, date: Long): String {
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
||||
|
||||
val pattern = when {
|
||||
now.year != then.year -> "dd MMM yyyy"
|
||||
now.month != then.month -> "dd MMMM"
|
||||
now.dayOfMonth != then.dayOfMonth -> {
|
||||
if (now.dayOfMonth - then.dayOfMonth == 1) {
|
||||
return resources.getString(R.string.yesterday)
|
||||
} else {
|
||||
"dd MMMM"
|
||||
}
|
||||
}
|
||||
|
||||
else -> return resources.getString(R.string.today)
|
||||
}
|
||||
|
||||
return SimpleDateFormat(pattern, Locale.getDefault()).format(date)
|
||||
}
|
||||
|
||||
fun getLocalizedTime(resources: Resources, date: Long): String {
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
||||
|
||||
return when {
|
||||
now.year != then.year -> {
|
||||
"${now.year - then.year}${resources.getString(R.string.year_short).lowercase()}"
|
||||
}
|
||||
|
||||
now.month != then.month -> {
|
||||
"${now.month - then.month}${resources.getString(R.string.month_short).lowercase()}"
|
||||
}
|
||||
|
||||
now.dayOfMonth != then.dayOfMonth -> {
|
||||
val change = now.dayOfMonth - then.dayOfMonth
|
||||
|
||||
if (change % 7 == 0) {
|
||||
"${change / 7}${resources.getString(R.string.week_short).lowercase()}"
|
||||
} else {
|
||||
"$change${resources.getString(R.string.day_short).lowercase()}"
|
||||
}
|
||||
}
|
||||
|
||||
now.hour == then.hour && now.minute == then.minute -> {
|
||||
resources.getString(R.string.time_now).lowercase()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
package com.meloda.app.fast.common.util
|
||||
|
||||
//import android.content.Context
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.AnnotatedString
|
||||
//import androidx.compose.ui.text.SpanStyle
|
||||
//import androidx.compose.ui.text.buildAnnotatedString
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.text.withStyle
|
||||
//import com.meloda.app.fast.common.UiImage
|
||||
//import com.meloda.app.fast.common.UiText
|
||||
//import com.meloda.app.fast.common.extensions.orDots
|
||||
//import com.meloda.app.fast.common.parseString
|
||||
//
|
||||
//
|
||||
//@Suppress("MemberVisibilityCanBePrivate")
|
||||
//object VkUtils {
|
||||
//
|
||||
// fun prepareMessageText(text: String, forConversations: Boolean = false): String {
|
||||
// return text.apply {
|
||||
// if (forConversations) {
|
||||
// replace("\n", " ")
|
||||
// }
|
||||
//
|
||||
// replace("&", "&")
|
||||
// replace(""", "\"")
|
||||
// replace("<br>", "\n")
|
||||
// replace(">", ">")
|
||||
// replace("<", "<")
|
||||
// replace("<br/>", "\n")
|
||||
// replace("–", "-")
|
||||
// trim()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun parseAttachments(baseAttachments: List<VkAttachmentItemData>?): List<VkAttachment>? {
|
||||
// if (baseAttachments.isNullOrEmpty()) return null
|
||||
//
|
||||
// val attachments = mutableListOf<VkAttachment>()
|
||||
//
|
||||
// for (baseAttachment in baseAttachments) {
|
||||
// when (baseAttachment.getPreparedType()) {
|
||||
// AttachmentType.UNKNOWN -> continue
|
||||
//
|
||||
// AttachmentType.PHOTO -> {
|
||||
// val photo = baseAttachment.photo ?: continue
|
||||
// attachments += photo.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.VIDEO -> {
|
||||
// val video = baseAttachment.video ?: continue
|
||||
// attachments += video.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.AUDIO -> {
|
||||
// val audio = baseAttachment.audio ?: continue
|
||||
// attachments += audio.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.FILE -> {
|
||||
// val file = baseAttachment.file ?: continue
|
||||
// attachments += file.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.LINK -> {
|
||||
// val link = baseAttachment.link ?: continue
|
||||
// attachments += link.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.MINI_APP -> {
|
||||
// val miniApp = baseAttachment.miniApp ?: continue
|
||||
// attachments += miniApp.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.AUDIO_MESSAGE -> {
|
||||
// val voiceMessage = baseAttachment.voiceMessage ?: continue
|
||||
// attachments += voiceMessage.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.STICKER -> {
|
||||
// val sticker = baseAttachment.sticker ?: continue
|
||||
// attachments += sticker.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.GIFT -> {
|
||||
// val gift = baseAttachment.gift ?: continue
|
||||
// attachments += gift.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.WALL -> {
|
||||
// val wall = baseAttachment.wall ?: continue
|
||||
// attachments += wall.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.GRAFFITI -> {
|
||||
// val graffiti = baseAttachment.graffiti ?: continue
|
||||
// attachments += graffiti.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.POLL -> {
|
||||
// val poll = baseAttachment.poll ?: continue
|
||||
// attachments += poll.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.WALL_REPLY -> {
|
||||
// val wallReply = baseAttachment.wallReply ?: continue
|
||||
// attachments += wallReply.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.CALL -> {
|
||||
// val call = baseAttachment.call ?: continue
|
||||
// attachments += call.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.GROUP_CALL_IN_PROGRESS -> {
|
||||
// val groupCall = baseAttachment.groupCall ?: continue
|
||||
// attachments += groupCall.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.CURATOR -> {
|
||||
// val curator = baseAttachment.curator ?: continue
|
||||
// attachments += curator.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.EVENT -> {
|
||||
// val event = baseAttachment.event ?: continue
|
||||
// attachments += event.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.STORY -> {
|
||||
// val story = baseAttachment.story ?: continue
|
||||
// attachments += story.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.WIDGET -> {
|
||||
// val widget = baseAttachment.widget ?: continue
|
||||
// attachments += widget.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.ARTIST -> {
|
||||
// val artist = baseAttachment.artist ?: continue
|
||||
// attachments += artist.toDomain()
|
||||
//
|
||||
// val audios = baseAttachment.audios ?: continue
|
||||
// audios.map(VkAudioData::toDomain).let(attachments::addAll)
|
||||
// }
|
||||
//
|
||||
// AttachmentType.AUDIO_PLAYLIST -> {
|
||||
// val audioPlaylist = baseAttachment.audioPlaylist ?: continue
|
||||
// attachments += audioPlaylist.toDomain()
|
||||
// }
|
||||
//
|
||||
// AttachmentType.PODCAST -> {
|
||||
// val podcast = baseAttachment.podcast ?: continue
|
||||
// attachments += podcast.toDomain()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return attachments
|
||||
// }
|
||||
//
|
||||
// fun getActionMessageText(
|
||||
// context: Context,
|
||||
// message: VkMessage?,
|
||||
// youPrefix: String,
|
||||
// messageUser: VkUserDomain?,
|
||||
// messageGroup: VkGroupDomain?,
|
||||
// action: VkMessage.Action?,
|
||||
// actionUser: VkUserDomain?,
|
||||
// actionGroup: VkGroupDomain?,
|
||||
// ): AnnotatedString? {
|
||||
// return when {
|
||||
// message == null -> null
|
||||
// action == null -> null
|
||||
//
|
||||
// else -> buildAnnotatedString {
|
||||
// when (action) {
|
||||
// VkMessage.Action.CHAT_CREATE -> {
|
||||
// val text = message.actionText ?: return null
|
||||
//
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isGroup() -> messageGroup?.name
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// val string = UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_created,
|
||||
// listOf(prefix, text)
|
||||
// ).parseString(context).orEmpty()
|
||||
//
|
||||
// append(string)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
//
|
||||
// val textStartIndex = string.indexOf(text)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = textStartIndex,
|
||||
// end = textStartIndex + text.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_TITLE_UPDATE -> {
|
||||
// val text = message.actionText ?: return null
|
||||
//
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isGroup() -> messageGroup?.name
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// val string = UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_renamed,
|
||||
// listOf(prefix, text)
|
||||
// ).parseString(context).orEmpty()
|
||||
//
|
||||
// append(string)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
//
|
||||
// val textStartIndex = string.indexOf(text)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = textStartIndex,
|
||||
// end = textStartIndex + text.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_PHOTO_UPDATE -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isGroup() -> messageGroup?.name
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_photo_update,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_PHOTO_REMOVE -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isGroup() -> messageGroup?.name
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_photo_remove,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_KICK_USER -> {
|
||||
// val memberId = message.actionMemberId ?: return null
|
||||
// val isUser = memberId > 0
|
||||
// val isGroup = memberId < 0
|
||||
//
|
||||
// if (isUser && actionUser == null) return null
|
||||
// if (isGroup && actionGroup == null) return null
|
||||
//
|
||||
// if (memberId == message.fromId) {
|
||||
// val prefix =
|
||||
// if (memberId == UserConfig.userId) youPrefix
|
||||
// else actionUser.toString()
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_user_left,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// } else {
|
||||
// val prefix =
|
||||
// if (message.fromId == UserConfig.userId) youPrefix
|
||||
// else messageUser?.toString() ?: messageGroup?.toString().orDots()
|
||||
//
|
||||
// val postfix =
|
||||
// if (memberId == UserConfig.userId) youPrefix.lowercase()
|
||||
// else actionUser.toString()
|
||||
//
|
||||
// val string = UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_user_kicked,
|
||||
// listOf(prefix, postfix)
|
||||
// ).parseString(context).orEmpty()
|
||||
//
|
||||
// append(string)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
//
|
||||
// val postfixStartIndex = string.indexOf(postfix)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = postfixStartIndex,
|
||||
// end = postfixStartIndex + postfix.length
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_INVITE_USER -> {
|
||||
// val memberId = message.actionMemberId ?: 0
|
||||
// val isUser = memberId > 0
|
||||
// val isGroup = memberId < 0
|
||||
//
|
||||
// if (isUser && actionUser == null) return null
|
||||
// if (isGroup && actionGroup == null) return null
|
||||
//
|
||||
// if (memberId == message.fromId) {
|
||||
// val prefix =
|
||||
// if (memberId == UserConfig.userId) youPrefix
|
||||
// else actionUser.toString()
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_user_returned,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// } else {
|
||||
// val prefix =
|
||||
// if (message.fromId == UserConfig.userId) youPrefix
|
||||
// else messageUser?.toString() ?: messageGroup?.toString().orDots()
|
||||
//
|
||||
// val postfix =
|
||||
// if (memberId == UserConfig.userId) youPrefix.lowercase()
|
||||
// else actionUser.toString()
|
||||
//
|
||||
// val string = UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_user_invited,
|
||||
// listOf(prefix, postfix)
|
||||
// ).parseString(context).orEmpty()
|
||||
//
|
||||
// append(string)
|
||||
//
|
||||
// val postfixStartIndex = string.indexOf(postfix)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = postfixStartIndex,
|
||||
// end = postfixStartIndex + postfix.length
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_user_joined_by_link,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_user_joined_by_call,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_user_joined_by_call_link,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_PIN_MESSAGE -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isGroup() -> messageGroup?.name
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_pin_message,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isGroup() -> messageGroup?.name
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_unpin_message,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_SCREENSHOT -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isGroup() -> messageGroup?.name
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_screenshot,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// VkMessage.Action.CHAT_STYLE_UPDATE -> {
|
||||
// val prefix = when {
|
||||
// message.fromId == UserConfig.userId -> youPrefix
|
||||
// message.isUser() -> messageUser?.toString()
|
||||
// else -> return null
|
||||
// } ?: return null
|
||||
//
|
||||
// UiText.ResourceParams(
|
||||
// UiR.string.message_action_chat_style_update,
|
||||
// listOf(prefix)
|
||||
// ).parseString(context).orEmpty().let(::append)
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
|
||||
// start = 0,
|
||||
// end = prefix.length
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun getForwardsText(context: Context, message: VkMessage?): AnnotatedString? {
|
||||
// return when {
|
||||
// message == null -> null
|
||||
//
|
||||
// message.hasForwards() -> buildAnnotatedString {
|
||||
// val forwards = message.forwards.orEmpty()
|
||||
//
|
||||
// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
// append(
|
||||
// UiText.Resource(
|
||||
// if (forwards.size == 1) UiR.string.forwarded_message
|
||||
// else UiR.string.forwarded_messages
|
||||
// ).parseString(context)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// else -> null
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun getAttachmentText(
|
||||
// getText: (UiText) -> String,
|
||||
// message: VkMessage?
|
||||
// ): AnnotatedString? {
|
||||
// return when {
|
||||
// message == null -> null
|
||||
//
|
||||
// message.geoType != null -> buildAnnotatedString {
|
||||
// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
// when (message.geoType) {
|
||||
// "point" -> getText(UiText.Resource(UiR.string.message_geo_point))
|
||||
// .let(::append)
|
||||
//
|
||||
// else -> getText(UiText.Resource(UiR.string.message_geo))
|
||||
// .let(::append)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// message.hasAttachments() -> buildAnnotatedString {
|
||||
// val attachments = message.attachments.orEmpty()
|
||||
//
|
||||
// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
// if (attachments.size == 1) {
|
||||
// getText(getAttachmentUiText(attachments.first())).let(::append)
|
||||
// } else {
|
||||
// when {
|
||||
// isAttachmentsHaveOneType(attachments) -> {
|
||||
// getText(getAttachmentUiText(attachments.first(), attachments.size))
|
||||
// .let(::append)
|
||||
// }
|
||||
//
|
||||
// attachments.any { it.type == AttachmentType.ARTIST } -> {
|
||||
// getText(
|
||||
// getAttachmentUiText(attachments.first { it.type == AttachmentType.ARTIST })
|
||||
// ).let(::append)
|
||||
// }
|
||||
//
|
||||
// else -> {
|
||||
// getText(UiText.Resource(UiR.string.message_attachments_many))
|
||||
// .let(::append)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// else -> null
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun getAttachmentConversationIcon(message: VkMessage?): UiImage? {
|
||||
// return message?.attachments?.let { attachments ->
|
||||
// if (attachments.isEmpty()) return null
|
||||
// if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
|
||||
// message.geoType?.let {
|
||||
// return UiImage.Resource(UiR.drawable.ic_map_marker)
|
||||
// }
|
||||
//
|
||||
// getAttachmentIconByType(attachments.first().type)
|
||||
// } else {
|
||||
// UiImage.Resource(UiR.drawable.ic_baseline_attach_file_24)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// fun getAttachmentUiText(
|
||||
// attachment: VkAttachment,
|
||||
// size: Int = 1,
|
||||
// ): UiText {
|
||||
// if (attachment.type.isMultiple()) {
|
||||
// return when (attachment.type) {
|
||||
// AttachmentType.PHOTO -> UiR.plurals.attachment_photos
|
||||
// AttachmentType.VIDEO -> UiR.plurals.attachment_videos
|
||||
// AttachmentType.AUDIO -> UiR.plurals.attachment_audios
|
||||
// AttachmentType.FILE -> UiR.plurals.attachment_files
|
||||
// else -> throw IllegalArgumentException("Unknown multiple type: ${attachment.type}")
|
||||
// }.let { resId -> UiText.QuantityResource(resId, size) }
|
||||
// }
|
||||
//
|
||||
// return when (attachment.type) {
|
||||
// AttachmentType.UNKNOWN,
|
||||
// AttachmentType.PHOTO,
|
||||
// AttachmentType.VIDEO,
|
||||
// AttachmentType.AUDIO,
|
||||
// AttachmentType.FILE -> {
|
||||
// throw IllegalArgumentException("Unknown multiple type: ${attachment.type}")
|
||||
// }
|
||||
//
|
||||
// AttachmentType.LINK -> UiR.string.message_attachments_link
|
||||
// AttachmentType.AUDIO_MESSAGE -> UiR.string.message_attachments_audio_message
|
||||
// AttachmentType.MINI_APP -> UiR.string.message_attachments_mini_app
|
||||
// AttachmentType.STICKER -> UiR.string.message_attachments_sticker
|
||||
// AttachmentType.GIFT -> UiR.string.message_attachments_gift
|
||||
// AttachmentType.WALL -> UiR.string.message_attachments_wall
|
||||
// AttachmentType.GRAFFITI -> UiR.string.message_attachments_graffiti
|
||||
// AttachmentType.POLL -> UiR.string.message_attachments_poll
|
||||
// AttachmentType.WALL_REPLY -> UiR.string.message_attachments_wall_reply
|
||||
// AttachmentType.CALL -> UiR.string.message_attachments_call
|
||||
// AttachmentType.GROUP_CALL_IN_PROGRESS -> UiR.string.message_attachments_call_in_progress
|
||||
// AttachmentType.CURATOR -> UiR.string.message_attachments_curator
|
||||
// AttachmentType.EVENT -> UiR.string.message_attachments_event
|
||||
// AttachmentType.STORY -> UiR.string.message_attachments_story
|
||||
// AttachmentType.WIDGET -> UiR.string.message_attachments_widget
|
||||
// AttachmentType.ARTIST -> UiR.string.message_attachments_artist
|
||||
// AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist
|
||||
// AttachmentType.PODCAST -> UiR.string.message_attachments_podcast
|
||||
// }.let(UiText::Resource)
|
||||
// }
|
||||
//
|
||||
// fun getTextWithVisualizedMentions(
|
||||
// originalText: String,
|
||||
// mentionColor: Color,
|
||||
// ): AnnotatedString = buildAnnotatedString {
|
||||
// val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
|
||||
//
|
||||
// val mentions = mutableListOf<MentionIndex>()
|
||||
//
|
||||
// var currentIndex = 0
|
||||
// val replacements = mutableListOf<Pair<IntRange, String>>()
|
||||
//
|
||||
// // TODO: 25/04/2024, Danil Nikolaev: check why not working ([id279494346|@iworld2rist] да убери ты Елену Шлипс от меня)
|
||||
// val result = regex.replace(originalText) { matchResult ->
|
||||
// val idPrefix = matchResult.groups[1]?.value.orEmpty()
|
||||
// val startIndex = matchResult.range.first
|
||||
// val endIndex = matchResult.range.last
|
||||
//
|
||||
// val id = matchResult.groups[2]?.value ?: ""
|
||||
// val text = matchResult.groups[3]?.value ?: ""
|
||||
//
|
||||
// val replaced =
|
||||
// text.substring(startIndex, endIndex + 1)
|
||||
// .replace("[$idPrefix$id|$text]", text)
|
||||
//
|
||||
// val indexRange =
|
||||
// (startIndex + currentIndex)..startIndex + currentIndex + replaced.length
|
||||
//
|
||||
// replacements.add(indexRange to replaced)
|
||||
//
|
||||
// mentions += MentionIndex(
|
||||
// id = id.toIntOrNull() ?: -1,
|
||||
// idPrefix = idPrefix,
|
||||
// indexRange = indexRange
|
||||
// )
|
||||
//
|
||||
// currentIndex += replaced.length - (endIndex - startIndex + 1)
|
||||
//
|
||||
// replaced
|
||||
// }
|
||||
//
|
||||
// append(result)
|
||||
//
|
||||
// mentions.forEach { mention ->
|
||||
// val startIndex = mention.indexRange.first
|
||||
// val endIndex = mention.indexRange.last
|
||||
//
|
||||
// addStyle(
|
||||
// style = SpanStyle(color = mentionColor),
|
||||
// start = startIndex,
|
||||
// end = endIndex
|
||||
// )
|
||||
// addStringAnnotation(
|
||||
// tag = mention.idPrefix,
|
||||
// annotation = mention.id.toString(),
|
||||
// start = startIndex,
|
||||
// end = endIndex
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="yesterday">Yesterday</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="year_short">Y</string>
|
||||
<string name="month_short">M</string>
|
||||
<string name="week_short">W</string>
|
||||
<string name="day_short">D</string>
|
||||
<string name="time_now">Now</string>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
}
|
||||
|
||||
group = "com.meloda.app.fast.data"
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.data"
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Configs.minSdk
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = Configs.java
|
||||
targetCompatibility = Configs.java
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Configs.java.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.common)
|
||||
api(projects.core.datastore)
|
||||
api(projects.core.model)
|
||||
api(projects.core.network)
|
||||
api(projects.core.database)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: research, maybe remove
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.eithernet)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,354 @@
|
||||
package com.meloda.app.fast.data
|
||||
|
||||
import android.util.Log
|
||||
import com.meloda.app.fast.common.UserConfig
|
||||
import com.meloda.app.fast.common.VkConstants
|
||||
import com.meloda.app.fast.common.extensions.asInt
|
||||
import com.meloda.app.fast.common.extensions.listenValue
|
||||
import com.meloda.app.fast.common.extensions.toList
|
||||
import com.meloda.app.fast.data.api.messages.MessagesUseCase
|
||||
import com.meloda.app.fast.model.ApiEvent
|
||||
import com.meloda.app.fast.model.InteractionType
|
||||
import com.meloda.app.fast.model.LongPollEvent
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class LongPollUpdatesParser(
|
||||
private val messagesUseCase: MessagesUseCase
|
||||
) {
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d("LongPollUpdatesParser", "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
private val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + job + exceptionHandler
|
||||
|
||||
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||
|
||||
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> =
|
||||
mutableMapOf()
|
||||
|
||||
fun parseNextUpdate(event: List<Any>) {
|
||||
val eventId = event.first().asInt()
|
||||
|
||||
val eventType: ApiEvent = try {
|
||||
ApiEvent.parse(eventId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
||||
return
|
||||
}
|
||||
|
||||
when (eventType) {
|
||||
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
|
||||
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
|
||||
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
|
||||
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event)
|
||||
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event)
|
||||
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event)
|
||||
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
|
||||
ApiEvent.PIN_UNPIN_CONVERSATION -> parseConversationPinStateChanged(eventType, event)
|
||||
|
||||
ApiEvent.TYPING,
|
||||
ApiEvent.AUDIO_MESSAGE_RECORDING,
|
||||
ApiEvent.PHOTO_UPLOADING,
|
||||
ApiEvent.VIDEO_UPLOADING,
|
||||
ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event)
|
||||
|
||||
ApiEvent.UNREAD_COUNT_UPDATE -> onNewEvent(eventType, event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNewEvent(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val interactionType = when (eventType) {
|
||||
ApiEvent.TYPING -> InteractionType.Typing
|
||||
ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage
|
||||
ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo
|
||||
ApiEvent.VIDEO_UPLOADING -> InteractionType.Video
|
||||
ApiEvent.FILE_UPLOADING -> InteractionType.File
|
||||
else -> return
|
||||
}
|
||||
|
||||
val peerId = event[1].asInt()
|
||||
val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId }
|
||||
val totalCount = event[3].asInt()
|
||||
val timestamp = event[4].asInt()
|
||||
|
||||
// if userIds contains only account's id, then we don't need to show our status
|
||||
if (userIds.isEmpty()) return
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[eventType]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.Interaction>)
|
||||
.onEvent(
|
||||
LongPollEvent.Interaction(
|
||||
interactionType = interactionType,
|
||||
peerId = peerId,
|
||||
userIds = userIds,
|
||||
totalCount = totalCount,
|
||||
timestamp = timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val peerId = event[1].asInt()
|
||||
val majorId = event[2].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkConversationPinStateChangedEvent(
|
||||
peerId = peerId,
|
||||
majorId = majorId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val messageId = event[1].asInt()
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val newMessageEvent: LongPollEvent.VkMessageNewEvent? =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
newMessageEvent?.let { event ->
|
||||
listenersMap[ApiEvent.MESSAGE_NEW]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
|
||||
.onEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val messageId = event[1].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
val editedMessageEvent: LongPollEvent.VkMessageEditEvent? =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
editedMessageEvent?.let { event ->
|
||||
listenersMap[ApiEvent.MESSAGE_EDIT]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
|
||||
.onEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val peerId = event[1].asInt()
|
||||
val messageId = event[2].asInt()
|
||||
val unreadCount = event[3].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadIncomingEvent(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
unreadCount = unreadCount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val peerId = event[1].asInt()
|
||||
val messageId = event[2].asInt()
|
||||
val unreadCount = event[3].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadOutgoingEvent(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
unreadCount = unreadCount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private suspend fun <T : LongPollEvent> loadNormalMessage(
|
||||
eventType: ApiEvent,
|
||||
messageId: Int
|
||||
): T? = suspendCoroutine {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
messagesUseCase.getById(
|
||||
messageId = messageId,
|
||||
extended = true,
|
||||
fields = VkConstants.ALL_FIELDS
|
||||
).listenValue(this) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error")
|
||||
},
|
||||
success = { response ->
|
||||
response?.let { message ->
|
||||
VkMemoryCache[message.id] = message
|
||||
messagesUseCase.storeMessage(message)
|
||||
|
||||
val resumeValue: LongPollEvent? = when (eventType) {
|
||||
ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message)
|
||||
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
resumeValue?.let { value -> it.resume(value as T) }
|
||||
} ?: it.resume(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> registerListener(
|
||||
eventType: ApiEvent,
|
||||
listener: VkEventCallback<T>
|
||||
) {
|
||||
listenersMap.let { map ->
|
||||
map[eventType] = (map[eventType] ?: mutableListOf()).also { it.add(listener) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> registerListeners(
|
||||
eventTypes: List<ApiEvent>,
|
||||
listener: VkEventCallback<T>
|
||||
) {
|
||||
eventTypes.forEach { eventType -> registerListener(eventType, listener) }
|
||||
}
|
||||
|
||||
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) {
|
||||
registerListener(ApiEvent.PIN_UNPIN_CONVERSATION, listener)
|
||||
}
|
||||
|
||||
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) {
|
||||
onConversationPinStateChanged(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener)
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
|
||||
onMessageIncomingRead(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener)
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
|
||||
onMessageOutgoingRead(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_NEW, listener)
|
||||
}
|
||||
|
||||
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
|
||||
onNewMessage(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_EDIT, listener)
|
||||
}
|
||||
|
||||
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
|
||||
onMessageEdited(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onInteractions(listener: VkEventCallback<LongPollEvent.Interaction>) {
|
||||
registerListeners(
|
||||
eventTypes = listOf(
|
||||
ApiEvent.TYPING,
|
||||
ApiEvent.AUDIO_MESSAGE_RECORDING,
|
||||
ApiEvent.PHOTO_UPLOADING,
|
||||
ApiEvent.VIDEO_UPLOADING,
|
||||
ApiEvent.FILE_UPLOADING
|
||||
),
|
||||
listener = listener
|
||||
)
|
||||
}
|
||||
|
||||
fun onInteractions(block: (LongPollEvent.Interaction) -> Unit) {
|
||||
onInteractions(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun clearListeners() {
|
||||
listenersMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <R : Any> assembleEventCallback(
|
||||
crossinline block: (R) -> Unit,
|
||||
): VkEventCallback<R> {
|
||||
return VkEventCallback { event -> block.invoke(event) }
|
||||
}
|
||||
|
||||
fun interface VkEventCallback<in T : Any> {
|
||||
fun onEvent(event: T)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.meloda.app.fast.data
|
||||
|
||||
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
||||
import com.meloda.app.fast.model.api.data.VkLongPollData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LongPollUseCase {
|
||||
|
||||
fun getLongPollServer(
|
||||
needPts: Boolean,
|
||||
version: Int
|
||||
): Flow<State<VkLongPollData>>
|
||||
|
||||
fun getLongPollUpdates(
|
||||
serverUrl: String,
|
||||
act: String = "a_check",
|
||||
key: String,
|
||||
ts: Int,
|
||||
wait: Int,
|
||||
mode: Int,
|
||||
version: Int
|
||||
): Flow<State<LongPollUpdates>>
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.meloda.app.fast.data
|
||||
|
||||
import com.meloda.app.fast.data.api.longpoll.LongPollRepository
|
||||
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
||||
import com.meloda.app.fast.model.api.data.VkLongPollData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class LongPollUseCaseImpl(
|
||||
private val repository: LongPollRepository
|
||||
) : LongPollUseCase {
|
||||
|
||||
override fun getLongPollServer(
|
||||
needPts: Boolean,
|
||||
version: Int
|
||||
): Flow<State<VkLongPollData>> = flow {
|
||||
emit(State.Loading)
|
||||
|
||||
val newState = repository.getLongPollServer(
|
||||
needPts = needPts,
|
||||
version = version
|
||||
).mapToState()
|
||||
|
||||
emit(newState)
|
||||
}
|
||||
|
||||
override fun getLongPollUpdates(
|
||||
serverUrl: String,
|
||||
act: String,
|
||||
key: String,
|
||||
ts: Int,
|
||||
wait: Int,
|
||||
mode: Int,
|
||||
version: Int
|
||||
): Flow<State<LongPollUpdates>> = flow {
|
||||
emit(State.Loading)
|
||||
|
||||
val newState = repository.getLongPollUpdates(
|
||||
serverUrl,
|
||||
act = act,
|
||||
key = key,
|
||||
ts = ts,
|
||||
wait = wait,
|
||||
mode = mode,
|
||||
version = version
|
||||
).mapToState()
|
||||
emit(newState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.meloda.app.fast.data
|
||||
|
||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
sealed class State<out T> {
|
||||
|
||||
data object Idle : State<Nothing>()
|
||||
data class Success<T>(val data: T) : State<T>()
|
||||
data object Loading : State<Nothing>()
|
||||
|
||||
sealed class Error : State<Nothing>() {
|
||||
|
||||
data class ApiError(
|
||||
val errorCode: Int,
|
||||
val errorMessage: String,
|
||||
) : Error()
|
||||
|
||||
data object ConnectionError : Error()
|
||||
|
||||
data object Unknown : Error()
|
||||
|
||||
data object InternalError : Error()
|
||||
|
||||
data class OAuthError(val error: OAuthErrorDomain) : Error()
|
||||
}
|
||||
|
||||
fun isLoading(): Boolean = this is Loading
|
||||
|
||||
companion object {
|
||||
|
||||
val UNKNOWN_ERROR = Error.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> State<T>.processState(
|
||||
error: (error: State.Error) -> (Unit),
|
||||
success: (data: T) -> (Unit),
|
||||
idle: (() -> (Unit)) = {},
|
||||
loading: (() -> (Unit)) = {},
|
||||
) {
|
||||
when (this) {
|
||||
is State.Error -> error(this)
|
||||
State.Idle -> idle()
|
||||
State.Loading -> loading()
|
||||
is State.Success -> success(data)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> Flow<State<T>>.mapSuccess(
|
||||
crossinline transform: suspend (value: T) -> R
|
||||
): Flow<R> = filterIsInstance<State.Success<T>>()
|
||||
.map { state -> transform.invoke(state.data) }
|
||||
|
||||
fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) {
|
||||
null -> State.Error.ConnectionError
|
||||
else -> State.Error.ApiError(code, message)
|
||||
}
|
||||
|
||||
fun OAuthErrorDomain?.toStateApiError(): State.Error = when (this) {
|
||||
null -> State.Error.ConnectionError
|
||||
else -> State.Error.OAuthError(this)
|
||||
}
|
||||
|
||||
fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
|
||||
is ApiResult.Success -> State.Success(this.value)
|
||||
|
||||
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
|
||||
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
|
||||
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
|
||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.meloda.app.fast.data
|
||||
|
||||
import com.meloda.app.fast.model.api.data.VkMessageData
|
||||
import com.meloda.app.fast.model.api.domain.VkConversation
|
||||
import com.meloda.app.fast.model.api.domain.VkGroupDomain
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import kotlin.math.abs
|
||||
|
||||
class VkGroupsMap(
|
||||
private val groups: List<VkGroupDomain>
|
||||
) {
|
||||
|
||||
private val map: HashMap<Int, VkGroupDomain> by lazy {
|
||||
HashMap(groups.associateBy(VkGroupDomain::id))
|
||||
}
|
||||
|
||||
fun groups(): List<VkGroupDomain> = map.values.toList()
|
||||
|
||||
fun conversationGroup(conversation: VkConversation): VkGroupDomain? =
|
||||
if (!conversation.peerType.isGroup()) null
|
||||
else map[abs(conversation.id)]
|
||||
|
||||
fun messageActionGroup(message: VkMessage): VkGroupDomain? =
|
||||
if (message.actionMemberId == null || message.actionMemberId!! >= 0) null
|
||||
else map[abs(message.actionMemberId!!)]
|
||||
|
||||
fun messageActionGroup(message: VkMessageData): VkGroupDomain? =
|
||||
if (message.action?.memberId == null || message.action!!.memberId!! >= 0) null
|
||||
else map[abs(message.action!!.memberId!!)]
|
||||
|
||||
fun messageGroup(message: VkMessage): VkGroupDomain? =
|
||||
if (!message.isGroup()) null
|
||||
else map[abs(message.fromId)]
|
||||
|
||||
fun messageGroup(message: VkMessageData): VkGroupDomain? =
|
||||
if (message.fromId >= 0) null
|
||||
else map[abs(message.fromId)]
|
||||
|
||||
fun group(groupId: Int): VkGroupDomain? = map[abs(groupId)]
|
||||
|
||||
companion object {
|
||||
|
||||
fun forGroups(groups: List<VkGroupDomain>): VkGroupsMap = VkGroupsMap(groups = groups)
|
||||
|
||||
fun List<VkGroupDomain>.toGroupsMap(): VkGroupsMap = forGroups(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.meloda.app.fast.data
|
||||
|
||||
import com.meloda.app.fast.model.api.domain.VkContactDomain
|
||||
import com.meloda.app.fast.model.api.domain.VkConversation
|
||||
import com.meloda.app.fast.model.api.domain.VkGroupDomain
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import kotlin.math.abs
|
||||
|
||||
object VkMemoryCache {
|
||||
|
||||
private val users: HashMap<Int, VkUser> = hashMapOf()
|
||||
private val groups: HashMap<Int, VkGroupDomain> = hashMapOf()
|
||||
private val messages: HashMap<Int, VkMessage> = hashMapOf()
|
||||
private val conversations: HashMap<Int, VkConversation> = hashMapOf()
|
||||
private val contacts: HashMap<Int, VkContactDomain> = hashMapOf()
|
||||
|
||||
fun appendUsers(users: List<VkUser>) {
|
||||
users.forEach { user -> VkMemoryCache.users[user.id] = user }
|
||||
}
|
||||
|
||||
fun appendGroups(groups: List<VkGroupDomain>) {
|
||||
groups.forEach { group -> VkMemoryCache.groups[abs(group.id)] = group }
|
||||
}
|
||||
|
||||
fun appendMessages(messages: List<VkMessage>) {
|
||||
messages.forEach { message -> VkMemoryCache.messages[message.id] = message }
|
||||
}
|
||||
|
||||
fun appendConversations(conversations: List<VkConversation>) {
|
||||
conversations.forEach { conversation ->
|
||||
VkMemoryCache.conversations[conversation.id] = conversation
|
||||
}
|
||||
}
|
||||
|
||||
fun appendContacts(contacts: List<VkContactDomain>) {
|
||||
contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact }
|
||||
}
|
||||
|
||||
operator fun set(userId: Int, user: VkUser) {
|
||||
users[userId] = user
|
||||
}
|
||||
|
||||
operator fun set(groupId: Int, group: VkGroupDomain) {
|
||||
groups[groupId] = group
|
||||
}
|
||||
|
||||
operator fun set(messageId: Int, message: VkMessage) {
|
||||
messages[messageId] = message
|
||||
}
|
||||
|
||||
operator fun set(conversationId: Int, conversation: VkConversation) {
|
||||
conversations[conversationId] = conversation
|
||||
}
|
||||
|
||||
operator fun set(contactId: Int, contact: VkContactDomain) {
|
||||
contacts[contactId] = contact
|
||||
}
|
||||
|
||||
fun getUser(id: Int): VkUser? {
|
||||
return getUsers(id).firstOrNull()
|
||||
}
|
||||
|
||||
fun getUsers(vararg ids: Int): List<VkUser> {
|
||||
return getUsers(ids.toList())
|
||||
}
|
||||
|
||||
fun getUsers(ids: List<Int>): List<VkUser> {
|
||||
return ids.mapNotNull { id -> users[id] }
|
||||
}
|
||||
|
||||
fun getGroup(id: Int): VkGroupDomain? {
|
||||
return getGroups(id).firstOrNull()
|
||||
}
|
||||
|
||||
fun getGroups(vararg ids: Int): List<VkGroupDomain> {
|
||||
return getGroups(ids.toList())
|
||||
}
|
||||
|
||||
fun getGroups(ids: List<Int>): List<VkGroupDomain> {
|
||||
return ids.mapNotNull { id -> groups[id] }
|
||||
}
|
||||
|
||||
fun getMessage(id: Int): VkMessage? {
|
||||
return getMessages(id).firstOrNull()
|
||||
}
|
||||
|
||||
fun getMessages(vararg ids: Int): List<VkMessage> {
|
||||
return getMessages(ids.toList())
|
||||
}
|
||||
|
||||
fun getMessages(ids: List<Int>): List<VkMessage> {
|
||||
return ids.mapNotNull { id -> messages[id] }
|
||||
}
|
||||
|
||||
fun getConversation(id: Int): VkConversation? {
|
||||
return getConversations(id).firstOrNull()
|
||||
}
|
||||
|
||||
fun getConversations(vararg ids: Int): List<VkConversation> {
|
||||
return getConversations(ids.toList())
|
||||
}
|
||||
|
||||
fun getConversations(ids: List<Int>): List<VkConversation> {
|
||||
return ids.mapNotNull { id -> conversations[id] }
|
||||
}
|
||||
|
||||
fun getContact(id: Int): VkContactDomain? {
|
||||
return getContacts(id).firstOrNull()
|
||||
}
|
||||
|
||||
fun getContacts(vararg ids: Int): List<VkContactDomain> {
|
||||
return getContacts(ids.toList())
|
||||
}
|
||||
|
||||
fun getContacts(ids: List<Int>): List<VkContactDomain> {
|
||||
return ids.mapNotNull { id -> contacts[id] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.meloda.app.fast.data
|
||||
|
||||
import com.meloda.app.fast.model.api.data.VkMessageData
|
||||
import com.meloda.app.fast.model.api.domain.VkConversation
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
|
||||
class VkUsersMap(
|
||||
private val users: List<VkUser>
|
||||
) {
|
||||
|
||||
private val map: HashMap<Int, VkUser> by lazy {
|
||||
HashMap(users.associateBy(VkUser::id))
|
||||
}
|
||||
|
||||
fun users(): List<VkUser> = map.values.toList()
|
||||
|
||||
fun conversationUser(conversation: VkConversation): VkUser? =
|
||||
if (!conversation.peerType.isUser()) null
|
||||
else map[conversation.id]
|
||||
|
||||
fun messageActionUser(message: VkMessage): VkUser? =
|
||||
if (message.actionMemberId == null || message.actionMemberId!! <= 0) null
|
||||
else map[message.actionMemberId]
|
||||
|
||||
fun messageActionUser(message: VkMessageData): VkUser? =
|
||||
if (message.action?.memberId == null || message.action!!.memberId!! <= 0) null
|
||||
else map[message.action!!.memberId]
|
||||
|
||||
fun messageUser(message: VkMessage): VkUser? =
|
||||
if (!message.isUser()) null
|
||||
else map[message.fromId]
|
||||
|
||||
fun messageUser(message: VkMessageData): VkUser? =
|
||||
if (message.fromId > 0) map[message.fromId]
|
||||
else null
|
||||
|
||||
fun user(userId: Int): VkUser? = map[userId]
|
||||
|
||||
companion object {
|
||||
|
||||
fun forUsers(users: List<VkUser>): VkUsersMap = VkUsersMap(users = users)
|
||||
|
||||
fun List<VkUser>.toUsersMap(): VkUsersMap = forUsers(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.app.fast.data.api.account
|
||||
|
||||
import com.meloda.app.fast.model.api.requests.AccountSetOfflineRequest
|
||||
import com.meloda.app.fast.model.api.requests.AccountSetOnlineRequest
|
||||
|
||||
interface AccountRepository {
|
||||
|
||||
suspend fun setOnline(
|
||||
params: AccountSetOnlineRequest
|
||||
): Boolean
|
||||
|
||||
suspend fun setOffline(
|
||||
params: AccountSetOfflineRequest
|
||||
): Boolean
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.meloda.app.fast.data.api.account
|
||||
|
||||
import com.meloda.app.fast.model.api.requests.AccountSetOfflineRequest
|
||||
import com.meloda.app.fast.model.api.requests.AccountSetOnlineRequest
|
||||
import com.meloda.app.fast.network.service.account.AccountService
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
class AccountRepositoryImpl(
|
||||
private val accountService: AccountService
|
||||
) : com.meloda.app.fast.data.api.account.AccountRepository {
|
||||
|
||||
override suspend fun setOnline(params: AccountSetOnlineRequest): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun setOffline(params: AccountSetOfflineRequest): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.meloda.app.fast.data.api.account
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AccountUseCase {
|
||||
|
||||
suspend fun setOnline(
|
||||
voip: Boolean,
|
||||
accessToken: String
|
||||
): Flow<State<Unit>>
|
||||
|
||||
suspend fun setOffline(
|
||||
accessToken: String
|
||||
): Flow<State<Unit>>
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.meloda.app.fast.data.api.account
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
class AccountUseCaseImpl(
|
||||
private val accountRepository: com.meloda.app.fast.data.api.account.AccountRepository
|
||||
) : com.meloda.app.fast.data.api.account.AccountUseCase {
|
||||
|
||||
override suspend fun setOnline(
|
||||
voip: Boolean,
|
||||
accessToken: String
|
||||
): Flow<State<Unit>> = flow {
|
||||
// emit(com.meloda.app.fast.data.State.Loading)
|
||||
//
|
||||
// val newState = accountRepository.setOnline(
|
||||
// params = AccountSetOnlineRequest(
|
||||
// voip = voip,
|
||||
// accessToken = accessToken
|
||||
// )
|
||||
// ).fold(
|
||||
// onSuccess = { response -> com.meloda.app.fast.data.State.Success(response) },
|
||||
// onNetworkFailure = { com.meloda.app.fast.data.State.Error.ConnectionError },
|
||||
// onUnknownFailure = { com.meloda.app.fast.data.State.UNKNOWN_ERROR },
|
||||
// onHttpFailure = { result -> result.error.toStateApiError() },
|
||||
// onApiFailure = { result -> result.error.toStateApiError() }
|
||||
// )
|
||||
// emit(newState)
|
||||
}
|
||||
|
||||
override suspend fun setOffline(
|
||||
accessToken: String
|
||||
): Flow<com.meloda.app.fast.data.State<Unit>> = flow {
|
||||
emit(com.meloda.app.fast.data.State.Loading)
|
||||
|
||||
// val newState = accountRepository.setOffline(
|
||||
// params = AccountSetOfflineRequest(accessToken = accessToken)
|
||||
// ).fold(
|
||||
// onSuccess = { response -> com.meloda.app.fast.data.State.Success(response) },
|
||||
// onNetworkFailure = { com.meloda.app.fast.data.State.Error.ConnectionError },
|
||||
// onUnknownFailure = { com.meloda.app.fast.data.State.UNKNOWN_ERROR },
|
||||
// onHttpFailure = { result -> result.error.toStateApiError() },
|
||||
// onApiFailure = { result -> result.error.toStateApiError() }
|
||||
// )
|
||||
// emit(newState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.meloda.app.fast.data.api.audios
|
||||
|
||||
import com.meloda.app.fast.model.api.responses.AudiosGetUploadServerResponse
|
||||
import com.meloda.app.fast.network.ApiResponse
|
||||
import com.meloda.app.fast.network.RestApiError
|
||||
import com.meloda.app.fast.network.service.audios.AudiosService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import okhttp3.MultipartBody
|
||||
|
||||
class AudiosRepository(
|
||||
private val audiosService: AudiosService
|
||||
) {
|
||||
|
||||
suspend fun getUploadServer(): ApiResult<ApiResponse<AudiosGetUploadServerResponse>, RestApiError> =
|
||||
audiosService.getUploadServer()
|
||||
|
||||
suspend fun upload(url: String, file: MultipartBody.Part) = audiosService.upload(url, file)
|
||||
|
||||
suspend fun save(server: Int, audio: String, hash: String) = audiosService.save(
|
||||
mapOf(
|
||||
"server" to server.toString(),
|
||||
"audio" to audio,
|
||||
"hash" to hash
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.meloda.app.fast.data.api.auth
|
||||
|
||||
import com.meloda.app.fast.model.api.requests.AuthDirectRequest
|
||||
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
|
||||
import com.meloda.app.fast.model.api.responses.SendSmsResponse
|
||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
interface AuthRepository {
|
||||
|
||||
// suspend fun auth(
|
||||
// params: AuthDirectRequest
|
||||
// ): ApiResult<AuthDirectResponse, OAuthErrorDomain>
|
||||
|
||||
suspend fun sendSms(
|
||||
validationSid: String
|
||||
): SendSmsResponse
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.meloda.app.fast.data.api.auth
|
||||
|
||||
import com.meloda.app.fast.model.api.requests.AuthDirectRequest
|
||||
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
|
||||
import com.meloda.app.fast.model.api.responses.SendSmsResponse
|
||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
||||
import com.meloda.app.fast.network.service.auth.AuthService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AuthRepositoryImpl(
|
||||
private val authService: AuthService
|
||||
) : AuthRepository {
|
||||
|
||||
// override suspend fun auth(
|
||||
// params: AuthDirectRequest
|
||||
// ): ApiResult<AuthDirectResponse, OAuthErrorDomain> {
|
||||
//
|
||||
// }
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
override suspend fun sendSms(
|
||||
validationSid: String
|
||||
): SendSmsResponse = withContext(Dispatchers.IO) {
|
||||
SendSmsResponse(
|
||||
validationSid = null, delay = null, validationType = null, validationResend = null
|
||||
|
||||
)
|
||||
// authService.sendSms(validationSid).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.meloda.app.fast.data.api.conversations
|
||||
|
||||
import com.meloda.app.fast.model.api.domain.VkConversation
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
interface ConversationsRepository {
|
||||
|
||||
suspend fun getConversations(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain>
|
||||
|
||||
suspend fun storeConversations(conversations: List<VkConversation>)
|
||||
suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain>
|
||||
suspend fun pin(peerId: Int): ApiResult<Int, RestApiErrorDomain>
|
||||
suspend fun unpin(peerId: Int): ApiResult<Int, RestApiErrorDomain>
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.meloda.app.fast.data.api.conversations
|
||||
|
||||
import com.meloda.app.fast.common.VkConstants
|
||||
import com.meloda.app.fast.data.VkGroupsMap
|
||||
import com.meloda.app.fast.data.VkMemoryCache
|
||||
import com.meloda.app.fast.data.VkUsersMap
|
||||
import com.meloda.app.fast.database.dao.ConversationDao
|
||||
import com.meloda.app.fast.model.api.data.VkContactData
|
||||
import com.meloda.app.fast.model.api.data.VkGroupData
|
||||
import com.meloda.app.fast.model.api.data.VkUserData
|
||||
import com.meloda.app.fast.model.api.data.asDomain
|
||||
import com.meloda.app.fast.model.api.domain.VkConversation
|
||||
import com.meloda.app.fast.model.api.domain.asEntity
|
||||
import com.meloda.app.fast.model.api.requests.ConversationsDeleteRequest
|
||||
import com.meloda.app.fast.model.api.requests.ConversationsGetRequest
|
||||
import com.meloda.app.fast.model.api.requests.ConversationsPinRequest
|
||||
import com.meloda.app.fast.model.api.requests.ConversationsUnpinRequest
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.meloda.app.fast.network.mapApiDefault
|
||||
import com.meloda.app.fast.network.mapApiResult
|
||||
import com.meloda.app.fast.network.service.conversations.ConversationsService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ConversationsRepositoryImpl(
|
||||
private val conversationsService: ConversationsService,
|
||||
private val conversationDao: ConversationDao
|
||||
) : ConversationsRepository {
|
||||
|
||||
override suspend fun getConversations(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = ConversationsGetRequest(
|
||||
count = count,
|
||||
offset = offset,
|
||||
fields = VkConstants.ALL_FIELDS,
|
||||
filter = "all",
|
||||
extended = true,
|
||||
startMessageId = null
|
||||
)
|
||||
|
||||
conversationsService.getConversations(requestModel.map).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
val response = apiResponse.requireResponse()
|
||||
|
||||
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
|
||||
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
|
||||
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
|
||||
|
||||
val usersMap = VkUsersMap.forUsers(profilesList)
|
||||
val groupsMap = VkGroupsMap.forGroups(groupsList)
|
||||
|
||||
VkMemoryCache.appendUsers(profilesList)
|
||||
VkMemoryCache.appendGroups(groupsList)
|
||||
VkMemoryCache.appendContacts(contactsList)
|
||||
|
||||
response.items.map { item ->
|
||||
val lastMessage = item.lastMessage?.asDomain()?.let { message ->
|
||||
message.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message)
|
||||
).also { VkMemoryCache[message.id] = it }
|
||||
}
|
||||
item.conversation.asDomain(lastMessage).let { conversation ->
|
||||
conversation.copy(
|
||||
user = usersMap.conversationUser(conversation),
|
||||
group = groupsMap.conversationGroup(conversation)
|
||||
).also { VkMemoryCache[conversation.id] = it }
|
||||
}
|
||||
}
|
||||
},
|
||||
errorMapper = { error ->
|
||||
error?.toDomain()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun storeConversations(conversations: List<VkConversation>) {
|
||||
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
|
||||
}
|
||||
|
||||
override suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val requestModel = ConversationsDeleteRequest(peerId = peerId)
|
||||
|
||||
conversationsService.delete(requestModel.map).mapApiResult(
|
||||
successMapper = { response -> response.requireResponse().lastDeletedId },
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun pin(
|
||||
peerId: Int
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = ConversationsPinRequest(peerId = peerId)
|
||||
conversationsService.pin(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun unpin(
|
||||
peerId: Int
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = ConversationsUnpinRequest(peerId = peerId)
|
||||
conversationsService.unpin(requestModel.map).mapApiDefault()
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.meloda.app.fast.data.api.conversations
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.model.api.domain.VkConversation
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ConversationsUseCase {
|
||||
|
||||
fun getConversations(
|
||||
count: Int?,
|
||||
offset: Int?,
|
||||
): Flow<State<List<VkConversation>>>
|
||||
|
||||
fun delete(peerId: Int): Flow<State<Int>>
|
||||
|
||||
fun changePinState(peerId: Int, pin: Boolean): Flow<State<Int>>
|
||||
|
||||
suspend fun storeConversations(conversations: List<VkConversation>)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.meloda.app.fast.data.api.files
|
||||
|
||||
import com.meloda.app.fast.network.service.files.FilesService
|
||||
import okhttp3.MultipartBody
|
||||
|
||||
class FilesRepository(
|
||||
private val filesService: FilesService
|
||||
) {
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: reimplement
|
||||
// enum class FileType(val value: String) {
|
||||
// @Json(name = "doc")
|
||||
// FILE("doc"),
|
||||
//
|
||||
// @Json(name = "audio_message")
|
||||
// AUDIO_MESSAGE("audio_message")
|
||||
// }
|
||||
//
|
||||
// suspend fun getMessagesUploadServer(peerId: Int, type: FileType) =
|
||||
// filesService.getUploadServer(
|
||||
// mapOf(
|
||||
// "peer_id" to peerId.toString(),
|
||||
// "type" to type.value
|
||||
// )
|
||||
// )
|
||||
|
||||
suspend fun uploadFile(url: String, file: MultipartBody.Part) = filesService.upload(url, file)
|
||||
|
||||
suspend fun saveMessageFile(file: String) = filesService.save(mapOf("file" to file))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.meloda.app.fast.data.api.friends
|
||||
|
||||
import com.meloda.app.fast.model.FriendsInfo
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
interface FriendsRepository {
|
||||
|
||||
suspend fun getAllFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<FriendsInfo, RestApiErrorDomain>
|
||||
|
||||
suspend fun getFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<List<VkUser>, RestApiErrorDomain>
|
||||
|
||||
suspend fun getOnlineFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<List<Int>, RestApiErrorDomain>
|
||||
|
||||
suspend fun storeUsers(users: List<VkUser>)
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.meloda.app.fast.data.api.friends
|
||||
|
||||
import com.meloda.app.fast.common.VkConstants
|
||||
import com.meloda.app.fast.data.VkMemoryCache
|
||||
import com.meloda.app.fast.database.dao.UsersDao
|
||||
import com.meloda.app.fast.model.FriendsInfo
|
||||
import com.meloda.app.fast.model.api.data.VkUserData
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import com.meloda.app.fast.model.api.domain.asEntity
|
||||
import com.meloda.app.fast.model.api.requests.GetFriendsRequest
|
||||
import com.meloda.app.fast.model.api.requests.GetOnlineFriendsRequest
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.meloda.app.fast.network.mapApiDefault
|
||||
import com.meloda.app.fast.network.mapApiResult
|
||||
import com.meloda.app.fast.network.service.friends.FriendsService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import com.slack.eithernet.successOrElse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FriendsRepositoryImpl(
|
||||
private val service: FriendsService,
|
||||
private val dao: UsersDao
|
||||
) : FriendsRepository {
|
||||
|
||||
override suspend fun getAllFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<FriendsInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val friends = async { getFriends(count, offset) }.await()
|
||||
.successOrElse { failure ->
|
||||
return@withContext failure
|
||||
}
|
||||
|
||||
val onlineFriends = async { getOnlineFriends(count, offset) }.await()
|
||||
.successOrElse { failure ->
|
||||
return@withContext failure
|
||||
}.mapNotNull { userId -> friends.find { it.id == userId } }
|
||||
|
||||
ApiResult.success(FriendsInfo(friends, onlineFriends))
|
||||
}
|
||||
|
||||
override suspend fun getFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = GetFriendsRequest(
|
||||
order = "hints",
|
||||
count = count,
|
||||
offset = offset,
|
||||
fields = VkConstants.USER_FIELDS
|
||||
)
|
||||
service.getFriends(requestModel.map).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
val response = apiResponse.requireResponse()
|
||||
val users = response.items.map(VkUserData::mapToDomain)
|
||||
|
||||
VkMemoryCache.appendUsers(users)
|
||||
|
||||
users
|
||||
},
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getOnlineFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = GetOnlineFriendsRequest(
|
||||
order = "hints",
|
||||
count = count,
|
||||
offset = offset,
|
||||
)
|
||||
|
||||
service.getOnlineFriends(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun storeUsers(users: List<VkUser>) = withContext(Dispatchers.IO) {
|
||||
dao.insertAll(users.map(VkUser::asEntity))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.meloda.app.fast.data.api.friends
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.model.FriendsInfo
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface FriendsUseCase {
|
||||
|
||||
fun getAllFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): Flow<State<FriendsInfo>>
|
||||
|
||||
fun getFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): Flow<State<List<VkUser>>>
|
||||
|
||||
fun getOnlineFriends(
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): Flow<State<List<Int>>>
|
||||
|
||||
suspend fun storeUsers(users: List<VkUser>)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.meloda.app.fast.data.api.longpoll
|
||||
|
||||
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
||||
import com.meloda.app.fast.model.api.data.VkLongPollData
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
interface LongPollRepository {
|
||||
|
||||
suspend fun getLongPollServer(
|
||||
needPts: Boolean,
|
||||
version: Int
|
||||
): ApiResult<VkLongPollData, RestApiErrorDomain>
|
||||
|
||||
suspend fun getLongPollUpdates(
|
||||
serverUrl: String,
|
||||
act: String,
|
||||
key: String,
|
||||
ts: Int,
|
||||
wait: Int,
|
||||
mode: Int,
|
||||
version: Int
|
||||
): ApiResult<LongPollUpdates, RestApiErrorDomain>
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.meloda.app.fast.data.api.longpoll
|
||||
|
||||
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
||||
import com.meloda.app.fast.model.api.data.VkLongPollData
|
||||
import com.meloda.app.fast.model.api.requests.LongPollGetUpdatesRequest
|
||||
import com.meloda.app.fast.model.api.requests.MessagesGetLongPollServerRequest
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.meloda.app.fast.network.mapApiResult
|
||||
import com.meloda.app.fast.network.mapResult
|
||||
import com.meloda.app.fast.network.service.longpoll.LongPollService
|
||||
import com.meloda.app.fast.network.service.messages.MessagesService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LongPollRepositoryImpl(
|
||||
private val longPollService: LongPollService,
|
||||
private val messagesService: MessagesService
|
||||
) : LongPollRepository {
|
||||
|
||||
override suspend fun getLongPollServer(
|
||||
needPts: Boolean,
|
||||
version: Int
|
||||
): ApiResult<VkLongPollData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesGetLongPollServerRequest(
|
||||
needPts = needPts,
|
||||
version = version
|
||||
)
|
||||
messagesService.getLongPollServer(requestModel.map).mapApiResult(
|
||||
successMapper = { response -> response.requireResponse() },
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getLongPollUpdates(
|
||||
serverUrl: String,
|
||||
act: String,
|
||||
key: String,
|
||||
ts: Int,
|
||||
wait: Int,
|
||||
mode: Int,
|
||||
version: Int
|
||||
): ApiResult<LongPollUpdates, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = LongPollGetUpdatesRequest(
|
||||
act = act,
|
||||
key = key,
|
||||
ts = ts,
|
||||
wait = wait,
|
||||
mode = mode,
|
||||
version = version
|
||||
)
|
||||
longPollService.getResponse(serverUrl, requestModel.map).mapResult(
|
||||
successMapper = { response -> response },
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.meloda.app.fast.data.api.messages
|
||||
|
||||
import com.meloda.app.fast.model.database.VkMessageEntity
|
||||
|
||||
interface MessagesLocalDataSource {
|
||||
|
||||
suspend fun getMessages(
|
||||
conversationId: Int,
|
||||
offset: Int?,
|
||||
count: Int?
|
||||
): List<VkMessageEntity>
|
||||
|
||||
suspend fun getMessage(messageId: Int): VkMessageEntity?
|
||||
|
||||
suspend fun storeMessages(messages: List<VkMessageEntity>)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.meloda.app.fast.data.api.messages
|
||||
|
||||
import com.meloda.app.fast.database.dao.MessageDao
|
||||
import com.meloda.app.fast.model.database.VkMessageEntity
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: use paging for room
|
||||
class MessagesLocalDataSourceImpl(
|
||||
private val messageDao: MessageDao
|
||||
) : MessagesLocalDataSource {
|
||||
|
||||
override suspend fun getMessages(
|
||||
conversationId: Int,
|
||||
offset: Int?,
|
||||
count: Int?
|
||||
): List<VkMessageEntity> = messageDao.getAll(conversationId)
|
||||
|
||||
override suspend fun getMessage(
|
||||
messageId: Int
|
||||
): VkMessageEntity? = messageDao.getById(messageId)
|
||||
|
||||
override suspend fun storeMessages(messages: List<VkMessageEntity>) {
|
||||
messageDao.insertAll(messages)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.meloda.app.fast.data.api.messages
|
||||
|
||||
import com.meloda.app.fast.model.api.domain.VkAttachment
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
interface MessagesNetworkDataSource {
|
||||
|
||||
suspend fun getMessagesHistory(
|
||||
conversationId: Int,
|
||||
offset: Int?,
|
||||
count: Int?,
|
||||
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain>
|
||||
|
||||
suspend fun getMessageById(
|
||||
messagesIds: List<Int>,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): ApiResult<VkMessage, RestApiErrorDomain>
|
||||
|
||||
suspend fun send(
|
||||
peerId: Int,
|
||||
randomId: Int,
|
||||
message: String?,
|
||||
replyTo: Int?,
|
||||
attachments: List<VkAttachment>?
|
||||
): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
suspend fun markAsRead(
|
||||
peerId: Int,
|
||||
startMessageId: Int?
|
||||
): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
suspend fun getMessage(messageId: Int): VkMessage?
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package com.meloda.app.fast.data.api.messages
|
||||
|
||||
import com.meloda.app.fast.common.VkConstants
|
||||
import com.meloda.app.fast.data.VkGroupsMap
|
||||
import com.meloda.app.fast.data.VkMemoryCache
|
||||
import com.meloda.app.fast.data.VkUsersMap
|
||||
import com.meloda.app.fast.model.api.data.VkContactData
|
||||
import com.meloda.app.fast.model.api.data.VkGroupData
|
||||
import com.meloda.app.fast.model.api.data.VkUserData
|
||||
import com.meloda.app.fast.model.api.data.asDomain
|
||||
import com.meloda.app.fast.model.api.domain.VkAttachment
|
||||
import com.meloda.app.fast.model.api.domain.VkConversation
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import com.meloda.app.fast.model.api.requests.MessagesGetByIdRequest
|
||||
import com.meloda.app.fast.model.api.requests.MessagesGetHistoryRequest
|
||||
import com.meloda.app.fast.model.api.requests.MessagesMarkAsReadRequest
|
||||
import com.meloda.app.fast.model.api.requests.MessagesSendRequest
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.meloda.app.fast.network.mapApiDefault
|
||||
import com.meloda.app.fast.network.mapApiResult
|
||||
import com.meloda.app.fast.network.service.messages.MessagesService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MessagesNetworkDataSourceImpl(
|
||||
private val messagesService: MessagesService
|
||||
) : MessagesNetworkDataSource {
|
||||
|
||||
override suspend fun getMessagesHistory(
|
||||
conversationId: Int,
|
||||
offset: Int?,
|
||||
count: Int?
|
||||
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesGetHistoryRequest(
|
||||
count = count,
|
||||
offset = offset,
|
||||
peerId = conversationId,
|
||||
extended = true,
|
||||
startMessageId = null,
|
||||
rev = null,
|
||||
fields = VkConstants.ALL_FIELDS
|
||||
)
|
||||
|
||||
messagesService.getHistory(requestModel.map).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
val response = apiResponse.requireResponse()
|
||||
|
||||
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
|
||||
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
|
||||
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
|
||||
|
||||
val usersMap = VkUsersMap.forUsers(profilesList)
|
||||
val groupsMap = VkGroupsMap.forGroups(groupsList)
|
||||
|
||||
VkMemoryCache.appendUsers(profilesList)
|
||||
VkMemoryCache.appendGroups(groupsList)
|
||||
VkMemoryCache.appendContacts(contactsList)
|
||||
|
||||
val messages = response.items.map { item ->
|
||||
item.asDomain().let { message ->
|
||||
message.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message)
|
||||
).also { VkMemoryCache[message.id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
val conversations = response.conversations.orEmpty().map { item ->
|
||||
val message = messages.firstOrNull { it.id == item.lastMessageId }
|
||||
item.asDomain(message)
|
||||
.let { conversation ->
|
||||
conversation.copy(
|
||||
user = usersMap.conversationUser(conversation),
|
||||
group = groupsMap.conversationGroup(conversation)
|
||||
).also { VkMemoryCache[conversation.id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
MessagesHistoryDomain(
|
||||
messages = messages,
|
||||
conversations = conversations
|
||||
)
|
||||
},
|
||||
errorMapper = { error ->
|
||||
error?.toDomain()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMessageById(
|
||||
messagesIds: List<Int>,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesGetByIdRequest(
|
||||
messagesIds = messagesIds,
|
||||
extended = extended,
|
||||
fields = fields
|
||||
)
|
||||
|
||||
messagesService.getById(requestModel.map).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
val response = apiResponse.requireResponse()
|
||||
|
||||
val message = response.items.single()
|
||||
val usersMap =
|
||||
VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain))
|
||||
val groupsMap =
|
||||
VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain))
|
||||
|
||||
message.asDomain().copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message)
|
||||
)
|
||||
},
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun send(
|
||||
peerId: Int,
|
||||
randomId: Int,
|
||||
message: String?,
|
||||
replyTo: Int?,
|
||||
attachments: List<VkAttachment>?
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesSendRequest(
|
||||
peerId = peerId,
|
||||
randomId = randomId,
|
||||
message = message,
|
||||
replyTo = replyTo,
|
||||
attachments = attachments
|
||||
)
|
||||
|
||||
messagesService.send(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun markAsRead(
|
||||
peerId: Int,
|
||||
startMessageId: Int?
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesMarkAsReadRequest(
|
||||
peerId = peerId,
|
||||
startMessageId = startMessageId
|
||||
)
|
||||
|
||||
messagesService.markAsRead(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun getMessage(messageId: Int): VkMessage? = withContext(Dispatchers.IO) {
|
||||
// TODO: 05/05/2024, Danil Nikolaev: get message
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
data class MessagesHistoryDomain(
|
||||
val messages: List<VkMessage>,
|
||||
val conversations: List<VkConversation>
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.meloda.app.fast.data.api.messages
|
||||
|
||||
import com.meloda.app.fast.model.api.domain.VkAttachment
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface MessagesRepository {
|
||||
|
||||
suspend fun getMessagesHistory(
|
||||
conversationId: Int,
|
||||
offset: Int?,
|
||||
count: Int?
|
||||
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain>
|
||||
|
||||
suspend fun getMessageById(
|
||||
messagesIds: List<Int>,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): ApiResult<VkMessage, RestApiErrorDomain>
|
||||
|
||||
suspend fun send(
|
||||
peerId: Int,
|
||||
randomId: Int,
|
||||
message: String?,
|
||||
replyTo: Int?,
|
||||
attachments: List<VkAttachment>?
|
||||
): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
suspend fun markAsRead(
|
||||
peerId: Int,
|
||||
startMessageId: Int?
|
||||
): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
suspend fun getMessage(messageId: Int): Flow<VkMessage?>
|
||||
|
||||
suspend fun storeMessages(messages: List<VkMessage>)
|
||||
|
||||
// suspend fun getHistory(
|
||||
// params: MessagesGetHistoryRequest
|
||||
// ): ApiResult<MessagesGetHistoryResponse, RestApiErrorDomain>
|
||||
|
||||
// suspend fun markAsImportant(
|
||||
// params: MessagesMarkAsImportantRequest
|
||||
// ): ApiResult<List<Int>, RestApiErrorDomain>
|
||||
//
|
||||
// suspend fun pin(
|
||||
// params: MessagesPinMessageRequest
|
||||
// ): ApiResult<VkMessageData, RestApiErrorDomain>
|
||||
//
|
||||
// suspend fun unpin(
|
||||
// params: MessagesUnPinMessageRequest
|
||||
// ): ApiResult<Unit, RestApiErrorDomain>
|
||||
//
|
||||
// suspend fun delete(
|
||||
// params: MessagesDeleteRequest
|
||||
// ): ApiResult<Unit, RestApiErrorDomain>
|
||||
//
|
||||
// suspend fun edit(
|
||||
// params: MessagesEditRequest
|
||||
// ): ApiResult<Int, RestApiErrorDomain>
|
||||
//
|
||||
// suspend fun getChat(
|
||||
// params: MessagesGetChatRequest
|
||||
// ): ApiResult<VkChatData, RestApiErrorDomain>
|
||||
//
|
||||
// suspend fun getConversationMembers(
|
||||
// params: MessagesGetConversationMembersRequest
|
||||
// ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
|
||||
//
|
||||
// suspend fun removeChatUser(
|
||||
// params: MessagesRemoveChatUserRequest
|
||||
// ): ApiResult<Int, RestApiErrorDomain>
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
package com.meloda.app.fast.data.api.messages
|
||||
|
||||
import com.meloda.app.fast.model.api.domain.VkAttachment
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import com.meloda.app.fast.model.api.domain.asEntity
|
||||
import com.meloda.app.fast.model.database.asExternalModel
|
||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement syncing
|
||||
class MessagesRepositoryImpl(
|
||||
private val networkDataSource: MessagesNetworkDataSource,
|
||||
private val localDataSource: MessagesLocalDataSource
|
||||
) : MessagesRepository {
|
||||
|
||||
override suspend fun getMessagesHistory(
|
||||
conversationId: Int,
|
||||
offset: Int?,
|
||||
count: Int?
|
||||
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// val localMessages = localDataSource.getMessages(
|
||||
// conversationId = conversationId,
|
||||
// offset = offset,
|
||||
// count = count
|
||||
// ).map(VkMessageEntity::asExternalModel)
|
||||
//
|
||||
// emit(localMessages)
|
||||
//
|
||||
// val networkMessages = networkDataSource.getMessagesHistory(
|
||||
// conversationId = conversationId,
|
||||
// offset = offset,
|
||||
// count = count
|
||||
// )
|
||||
//
|
||||
// emit(networkMessages)
|
||||
|
||||
networkDataSource.getMessagesHistory(conversationId, offset, count)
|
||||
}
|
||||
|
||||
override suspend fun getMessageById(
|
||||
messagesIds: List<Int>,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
networkDataSource.getMessageById(
|
||||
messagesIds = messagesIds,
|
||||
extended = extended,
|
||||
fields = fields
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun send(
|
||||
peerId: Int,
|
||||
randomId: Int,
|
||||
message: String?,
|
||||
replyTo: Int?,
|
||||
attachments: List<VkAttachment>?
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
networkDataSource.send(
|
||||
peerId,
|
||||
randomId,
|
||||
message,
|
||||
replyTo,
|
||||
attachments
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun markAsRead(
|
||||
peerId: Int,
|
||||
startMessageId: Int?
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
networkDataSource.markAsRead(peerId, startMessageId)
|
||||
}
|
||||
|
||||
override suspend fun getMessage(messageId: Int): Flow<VkMessage?> = flow {
|
||||
val localMessage = localDataSource.getMessage(messageId)?.asExternalModel()
|
||||
|
||||
emit(localMessage)
|
||||
|
||||
val networkMessage = networkDataSource.getMessage(messageId)
|
||||
|
||||
emit(networkMessage)
|
||||
}
|
||||
|
||||
override suspend fun storeMessages(messages: List<VkMessage>) {
|
||||
localDataSource.storeMessages(messages.map(VkMessage::asEntity))
|
||||
}
|
||||
|
||||
// override suspend fun getHistory(
|
||||
// params: MessagesGetHistoryRequest
|
||||
// ): ApiResult<MessagesGetHistoryResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.getHistory(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun send(
|
||||
// params: MessagesSendRequest
|
||||
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.send(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun markAsImportant(
|
||||
// params: MessagesMarkAsImportantRequest
|
||||
// ): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.markAsImportant(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun pin(
|
||||
// params: MessagesPinMessageRequest
|
||||
// ): ApiResult<VkMessageData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.pin(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun unpin(
|
||||
// params: MessagesUnPinMessageRequest
|
||||
// ): ApiResult<Unit, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.unpin(params.map).mapResult(
|
||||
// successMapper = {},
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun delete(
|
||||
// params: MessagesDeleteRequest
|
||||
// ): ApiResult<Unit, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.delete(params.map).mapResult(
|
||||
// successMapper = {},
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun edit(
|
||||
// params: MessagesEditRequest
|
||||
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.edit(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun getById(
|
||||
// params: MessagesGetByIdRequest
|
||||
// ): ApiResult<MessagesGetByIdResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.getById(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun markAsRead(
|
||||
// params: MessagesMarkAsReadRequest
|
||||
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.markAsRead(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun getChat(
|
||||
// params: MessagesGetChatRequest
|
||||
// ): ApiResult<VkChatData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.getChat(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun getConversationMembers(
|
||||
// params: MessagesGetConversationMembersRequest
|
||||
// ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> =
|
||||
// withContext(Dispatchers.IO) {
|
||||
// messagesService.getConversationMembers(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// override suspend fun removeChatUser(
|
||||
// params: MessagesRemoveChatUserRequest
|
||||
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
// messagesService.removeChatUser(params.map).mapResult(
|
||||
// successMapper = { response -> response.requireResponse() },
|
||||
// errorMapper = { error -> error?.toDomain() }
|
||||
// )
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.meloda.app.fast.data.api.messages
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.model.api.domain.VkAttachment
|
||||
import com.meloda.app.fast.model.api.domain.VkMessage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface MessagesUseCase {
|
||||
|
||||
fun getMessagesHistory(
|
||||
conversationId: Int,
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): Flow<State<MessagesHistoryDomain>>
|
||||
|
||||
fun getById(
|
||||
messageId: Int,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): Flow<State<VkMessage?>>
|
||||
|
||||
fun getByIds(
|
||||
messageIds: List<Int>,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): Flow<State<List<VkMessage>>>
|
||||
|
||||
fun sendMessage(
|
||||
peerId: Int,
|
||||
randomId: Int,
|
||||
message: String?,
|
||||
replyTo: Int?,
|
||||
attachments: List<VkAttachment>?
|
||||
): Flow<State<Int>>
|
||||
|
||||
fun markAsRead(
|
||||
peerId: Int,
|
||||
startMessageId: Int
|
||||
): Flow<State<Int>>
|
||||
|
||||
suspend fun storeMessage(message: VkMessage)
|
||||
suspend fun storeMessages(messages: List<VkMessage>)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.app.fast.data.api.oauth
|
||||
|
||||
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
|
||||
|
||||
interface OAuthRepository {
|
||||
|
||||
suspend fun auth(
|
||||
login: String,
|
||||
password: String,
|
||||
forceSms: Boolean,
|
||||
twoFaCode: String?,
|
||||
captchaSid: String?,
|
||||
captchaKey: String?
|
||||
): AuthDirectResponse
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.meloda.app.fast.data.api.oauth
|
||||
|
||||
import com.meloda.app.fast.common.VkConstants
|
||||
import com.meloda.app.fast.model.api.requests.AuthDirectRequest
|
||||
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
|
||||
import com.meloda.app.fast.network.service.oauth.OAuthService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class OAuthRepositoryImpl(
|
||||
private val oAuthService: OAuthService,
|
||||
) : OAuthRepository {
|
||||
|
||||
override suspend fun auth(
|
||||
login: String,
|
||||
password: String,
|
||||
forceSms: Boolean,
|
||||
twoFaCode: String?,
|
||||
captchaSid: String?,
|
||||
captchaKey: String?
|
||||
): AuthDirectResponse = withContext(Dispatchers.IO) {
|
||||
val requestModel = AuthDirectRequest(
|
||||
grantType = VkConstants.Auth.GrantType.PASSWORD,
|
||||
clientId = VkConstants.VK_APP_ID,
|
||||
clientSecret = VkConstants.VK_SECRET,
|
||||
username = login,
|
||||
password = password,
|
||||
scope = VkConstants.Auth.SCOPE,
|
||||
twoFaForceSms = forceSms,
|
||||
twoFaCode = twoFaCode,
|
||||
captchaSid = captchaSid,
|
||||
captchaKey = captchaKey,
|
||||
)
|
||||
|
||||
when (val result = oAuthService.auth(requestModel.map)) {
|
||||
is ApiResult.Success -> result.value
|
||||
|
||||
is ApiResult.Failure.HttpFailure -> {
|
||||
requireNotNull(result.error)
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unknown result")
|
||||
|
||||
// is ApiResult.Failure.ApiFailure -> TODO()
|
||||
// is ApiResult.Failure.HttpFailure -> TODO()
|
||||
// is ApiResult.Failure.NetworkFailure -> TODO()
|
||||
// is ApiResult.Failure.UnknownFailure -> TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.meloda.app.fast.data.api.photos
|
||||
|
||||
import com.meloda.app.fast.model.api.requests.PhotosSaveMessagePhotoRequest
|
||||
import com.meloda.app.fast.network.service.photos.PhotosService
|
||||
import okhttp3.MultipartBody
|
||||
|
||||
class PhotosRepository(
|
||||
private val photosService: PhotosService
|
||||
) {
|
||||
|
||||
suspend fun getMessagesUploadServer(peerId: Int) =
|
||||
photosService.getUploadServer(mapOf("peer_id" to peerId.toString()))
|
||||
|
||||
suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) =
|
||||
photosService.upload(url, photo)
|
||||
|
||||
suspend fun saveMessagePhoto(body: PhotosSaveMessagePhotoRequest) =
|
||||
photosService.save(body.map)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.meloda.app.fast.data.api.users
|
||||
|
||||
import com.meloda.app.fast.model.api.data.VkUserData
|
||||
import com.meloda.app.fast.model.api.requests.UsersGetRequest
|
||||
|
||||
interface UsersRepository {
|
||||
suspend fun getById(params: UsersGetRequest): List<VkUserData>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.meloda.app.fast.data.api.users
|
||||
|
||||
import com.meloda.app.fast.model.api.data.VkUserData
|
||||
import com.meloda.app.fast.model.api.requests.UsersGetRequest
|
||||
import com.meloda.app.fast.network.service.users.UsersService
|
||||
|
||||
class UsersRepositoryImpl(
|
||||
private val usersService: UsersService
|
||||
) : UsersRepository {
|
||||
|
||||
override suspend fun getById(params: UsersGetRequest): List<VkUserData> {
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.meloda.app.fast.data.api.users
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UsersUseCase {
|
||||
|
||||
fun getUserById(
|
||||
userId: Int,
|
||||
fields: String?,
|
||||
nomCase: String?
|
||||
): Flow<State<VkUser?>>
|
||||
|
||||
fun getUsersByIds(
|
||||
userIds: List<Int>,
|
||||
fields: String?,
|
||||
nomCase: String?
|
||||
): Flow<State<List<VkUser>>>
|
||||
|
||||
suspend fun storeUser(user: VkUser)
|
||||
suspend fun storeUsers(users: List<VkUser>)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.meloda.app.fast.data.api.users
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.model.api.domain.VkUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
class UsersUseCaseImpl(
|
||||
private val usersRepository: UsersRepository,
|
||||
) : UsersUseCase {
|
||||
|
||||
override fun getUserById(
|
||||
userId: Int,
|
||||
fields: String?,
|
||||
nomCase: String?
|
||||
): Flow<State<VkUser?>> = flow {
|
||||
// emit(State.Loading)
|
||||
//
|
||||
// val newState = usersRepository.getById(
|
||||
// UsersGetRequest(
|
||||
// userIds = listOf(userId),
|
||||
// fields = fields,
|
||||
// nomCase = nomCase
|
||||
// )
|
||||
// ).fold(
|
||||
// onSuccess = { response -> State.Success(response.singleOrNull()?.mapToDomain()) },
|
||||
// onNetworkFailure = { State.Error.ConnectionError },
|
||||
// onUnknownFailure = { State.UNKNOWN_ERROR },
|
||||
// onHttpFailure = { result -> result.error.toStateApiError() },
|
||||
// onApiFailure = { result -> result.error.toStateApiError() }
|
||||
// )
|
||||
// emit(newState)
|
||||
}
|
||||
|
||||
override fun getUsersByIds(
|
||||
userIds: List<Int>,
|
||||
fields: String?,
|
||||
nomCase: String?
|
||||
): Flow<State<List<VkUser>>> = flow {
|
||||
// emit(State.Loading)
|
||||
//
|
||||
// val newState = usersRepository.getById(
|
||||
// UsersGetRequest(
|
||||
// userIds = userIds,
|
||||
// fields = fields,
|
||||
// nomCase = nomCase
|
||||
// )
|
||||
// ).fold(
|
||||
// onSuccess = { response -> State.Success(response.map(VkUserData::mapToDomain)) },
|
||||
// onNetworkFailure = { State.Error.ConnectionError },
|
||||
// onUnknownFailure = { State.UNKNOWN_ERROR },
|
||||
// onHttpFailure = { result -> result.error.toStateApiError() },
|
||||
// onApiFailure = { result -> result.error.toStateApiError() }
|
||||
// )
|
||||
// emit(newState)
|
||||
}
|
||||
|
||||
override suspend fun storeUser(user: VkUser) {
|
||||
|
||||
}
|
||||
|
||||
override suspend fun storeUsers(users: List<VkUser>) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.meloda.app.fast.data.api.videos
|
||||
|
||||
import com.meloda.app.fast.network.service.videos.VideosService
|
||||
import okhttp3.MultipartBody
|
||||
|
||||
class VideosRepository(
|
||||
private val videosService: VideosService
|
||||
) {
|
||||
|
||||
suspend fun save() = videosService.save()
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: research, maybe remove multipart.body
|
||||
suspend fun upload(url: String, file: MultipartBody.Part) = videosService.upload(url, file)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.meloda.app.fast.data.db
|
||||
|
||||
import com.meloda.app.fast.model.database.AccountEntity
|
||||
|
||||
interface AccountsRepository {
|
||||
|
||||
suspend fun getAccounts(): List<AccountEntity>
|
||||
|
||||
suspend fun storeAccounts(accounts: List<AccountEntity>)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.app.fast.data.db
|
||||
|
||||
import com.meloda.app.fast.database.dao.AccountDao
|
||||
import com.meloda.app.fast.model.database.AccountEntity
|
||||
|
||||
class AccountsRepositoryImpl(
|
||||
private val accountDao: AccountDao
|
||||
) : AccountsRepository {
|
||||
|
||||
override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll()
|
||||
|
||||
override suspend fun storeAccounts(
|
||||
accounts: List<AccountEntity>
|
||||
) = accountDao.insertAll(accounts)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.meloda.app.fast.data.di
|
||||
|
||||
import com.meloda.app.fast.common.di.commonModule
|
||||
import com.meloda.app.fast.data.api.account.AccountRepository
|
||||
import com.meloda.app.fast.data.api.account.AccountRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.account.AccountUseCase
|
||||
import com.meloda.app.fast.data.api.account.AccountUseCaseImpl
|
||||
import com.meloda.app.fast.data.api.audios.AudiosRepository
|
||||
import com.meloda.app.fast.data.api.auth.AuthRepository
|
||||
import com.meloda.app.fast.data.api.auth.AuthRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.conversations.ConversationsRepository
|
||||
import com.meloda.app.fast.data.api.conversations.ConversationsRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.files.FilesRepository
|
||||
import com.meloda.app.fast.data.api.friends.FriendsRepository
|
||||
import com.meloda.app.fast.data.api.friends.FriendsRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.longpoll.LongPollRepository
|
||||
import com.meloda.app.fast.data.api.longpoll.LongPollRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.messages.MessagesLocalDataSource
|
||||
import com.meloda.app.fast.data.api.messages.MessagesLocalDataSourceImpl
|
||||
import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSource
|
||||
import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSourceImpl
|
||||
import com.meloda.app.fast.data.api.messages.MessagesRepository
|
||||
import com.meloda.app.fast.data.api.messages.MessagesRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.oauth.OAuthRepository
|
||||
import com.meloda.app.fast.data.api.oauth.OAuthRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.photos.PhotosRepository
|
||||
import com.meloda.app.fast.data.api.users.UsersRepository
|
||||
import com.meloda.app.fast.data.api.users.UsersRepositoryImpl
|
||||
import com.meloda.app.fast.data.api.users.UsersUseCase
|
||||
import com.meloda.app.fast.data.api.users.UsersUseCaseImpl
|
||||
import com.meloda.app.fast.data.api.videos.VideosRepository
|
||||
import com.meloda.app.fast.data.db.AccountsRepository
|
||||
import com.meloda.app.fast.data.db.AccountsRepositoryImpl
|
||||
import com.meloda.app.fast.database.di.databaseModule
|
||||
import com.meloda.app.fast.datastore.di.dataStoreModule
|
||||
import com.meloda.app.fast.network.di.networkModule
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val dataModule = module {
|
||||
includes(
|
||||
commonModule,
|
||||
databaseModule,
|
||||
dataStoreModule,
|
||||
networkModule,
|
||||
)
|
||||
|
||||
singleOf(::AccountRepositoryImpl) bind AccountRepository::class
|
||||
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
|
||||
|
||||
singleOf(::AudiosRepository)
|
||||
|
||||
singleOf(::AuthRepositoryImpl) bind AuthRepository::class
|
||||
|
||||
singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class
|
||||
|
||||
singleOf(::FilesRepository)
|
||||
|
||||
singleOf(::LongPollRepositoryImpl) bind LongPollRepository::class
|
||||
|
||||
singleOf(::MessagesLocalDataSourceImpl) bind MessagesLocalDataSource::class
|
||||
singleOf(::MessagesNetworkDataSourceImpl) bind MessagesNetworkDataSource::class
|
||||
singleOf(::MessagesRepositoryImpl) bind MessagesRepository::class
|
||||
|
||||
singleOf(::OAuthRepositoryImpl) bind OAuthRepository::class
|
||||
|
||||
singleOf(::PhotosRepository)
|
||||
|
||||
singleOf(::UsersRepositoryImpl) bind UsersRepository::class
|
||||
singleOf(::UsersUseCaseImpl) bind UsersUseCase::class
|
||||
|
||||
singleOf(::VideosRepository)
|
||||
|
||||
singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class
|
||||
|
||||
singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.com.google.devtools.ksp)
|
||||
}
|
||||
|
||||
group = "com.meloda.app.fast.database"
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.database"
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Configs.minSdk
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = Configs.java
|
||||
targetCompatibility = Configs.java
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Configs.java.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.model)
|
||||
|
||||
implementation(libs.room.ktx)
|
||||
implementation(libs.room.runtime)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.meloda.app.fast.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.meloda.app.fast.database.dao.AccountDao
|
||||
import com.meloda.app.fast.model.database.AccountEntity
|
||||
|
||||
@Database(
|
||||
entities = [AccountEntity::class],
|
||||
version = 2
|
||||
)
|
||||
abstract class AccountsDatabase : RoomDatabase() {
|
||||
abstract fun accountDao(): AccountDao
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.meloda.app.fast.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.meloda.app.fast.database.dao.ConversationDao
|
||||
import com.meloda.app.fast.database.dao.GroupDao
|
||||
import com.meloda.app.fast.database.dao.MessageDao
|
||||
import com.meloda.app.fast.database.dao.UsersDao
|
||||
import com.meloda.app.fast.database.typeconverters.Converters
|
||||
import com.meloda.app.fast.model.database.VkConversationEntity
|
||||
import com.meloda.app.fast.model.database.VkGroupEntity
|
||||
import com.meloda.app.fast.model.database.VkMessageEntity
|
||||
import com.meloda.app.fast.model.database.VkUserEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
VkUserEntity::class,
|
||||
VkGroupEntity::class,
|
||||
VkMessageEntity::class,
|
||||
VkConversationEntity::class
|
||||
],
|
||||
|
||||
version = 5
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class CacheDatabase : RoomDatabase() {
|
||||
abstract fun userDao(): UsersDao
|
||||
abstract fun groupDao(): GroupDao
|
||||
abstract fun messageDao(): MessageDao
|
||||
abstract fun conversationDao(): ConversationDao
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.app.fast.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.meloda.app.fast.model.database.AccountEntity
|
||||
|
||||
@Dao
|
||||
abstract class AccountDao : EntityDao<AccountEntity> {
|
||||
|
||||
@Query("SELECT * FROM accounts")
|
||||
abstract suspend fun getAll(): List<AccountEntity>
|
||||
|
||||
@Query("DELETE FROM accounts WHERE userId = :userId")
|
||||
abstract suspend fun deleteById(userId: Int)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.meloda.app.fast.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.meloda.app.fast.model.database.ConversationWithMessage
|
||||
import com.meloda.app.fast.model.database.VkConversationEntity
|
||||
|
||||
@Dao
|
||||
abstract class ConversationDao : EntityDao<VkConversationEntity> {
|
||||
|
||||
@Query("SELECT * FROM conversations")
|
||||
abstract suspend fun getAll(): List<VkConversationEntity>
|
||||
|
||||
@Query("SELECT * FROM conversations WHERE id IN (:ids)")
|
||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity>
|
||||
|
||||
@Query("SELECT * FROM conversations WHERE id IS (:id)")
|
||||
abstract suspend fun getById(id: Int): VkConversationEntity?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM conversations WHERE id IS (:id)")
|
||||
abstract suspend fun getByIdWithMessage(id: Int): ConversationWithMessage?
|
||||
|
||||
@Query("DELETE FROM conversations WHERE rowid IN (:ids)")
|
||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.meloda.app.fast.database.dao
|
||||
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
|
||||
interface EntityDao<T> {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(values: List<T>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(value: T)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(value: T): Int
|
||||
|
||||
@Delete
|
||||
suspend fun deleteAll(values: List<T>): Int
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.meloda.app.fast.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.meloda.app.fast.model.database.VkGroupEntity
|
||||
|
||||
@Dao
|
||||
abstract class GroupDao : EntityDao<VkGroupEntity> {
|
||||
|
||||
@Query("SELECT * FROM groups")
|
||||
abstract suspend fun getAll(): List<VkGroupEntity>
|
||||
|
||||
@Query("SELECT * FROM groups WHERE id IN (:ids)")
|
||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkGroupEntity>
|
||||
|
||||
@Query("DELETE FROM groups WHERE id IN (:ids)")
|
||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.meloda.app.fast.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.meloda.app.fast.model.database.VkMessageEntity
|
||||
|
||||
@Dao
|
||||
abstract class MessageDao : EntityDao<VkMessageEntity> {
|
||||
|
||||
@Query("SELECT * FROM messages")
|
||||
abstract suspend fun getAll(): List<VkMessageEntity>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE peerId IS (:conversationId)")
|
||||
abstract suspend fun getAll(conversationId: Int): List<VkMessageEntity>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE id IN (:ids)")
|
||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE id IS (:messageId)")
|
||||
abstract suspend fun getById(messageId: Int): VkMessageEntity?
|
||||
|
||||
@Query("DELETE FROM messages WHERE id IN (:ids)")
|
||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.meloda.app.fast.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.meloda.app.fast.model.database.VkUserEntity
|
||||
|
||||
@Dao
|
||||
abstract class UsersDao : EntityDao<VkUserEntity> {
|
||||
|
||||
@Query("SELECT * FROM users")
|
||||
abstract suspend fun getAll(): List<VkUserEntity>
|
||||
|
||||
@Query("SELECT * FROM users WHERE id IN (:ids)")
|
||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkUserEntity>
|
||||
|
||||
@Query("DELETE FROM users WHERE id IN (:ids)")
|
||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.meloda.app.fast.database.di
|
||||
|
||||
import androidx.room.Room
|
||||
import com.meloda.app.fast.database.AccountsDatabase
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
|
||||
val databaseModule = module {
|
||||
single {
|
||||
Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts").build()
|
||||
}
|
||||
single { get<AccountsDatabase>().accountDao() }
|
||||
|
||||
single {
|
||||
Room.databaseBuilder(get(), com.meloda.app.fast.database.CacheDatabase::class.java, "cache")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
single { cacheDB().userDao() }
|
||||
single { cacheDB().groupDao() }
|
||||
single { cacheDB().messageDao() }
|
||||
single { cacheDB().conversationDao() }
|
||||
}
|
||||
|
||||
private fun Scope.cacheDB(): com.meloda.app.fast.database.CacheDatabase = get()
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.meloda.app.fast.database.typeconverters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
|
||||
class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun intListToString(list: List<Int>): String = list.joinToString()
|
||||
|
||||
@TypeConverter
|
||||
fun stringToIntList(string: String): List<Int> =
|
||||
string
|
||||
.split(", ")
|
||||
.mapNotNull(String::toIntOrNull)
|
||||
|
||||
@TypeConverter
|
||||
fun stringListToString(list: List<String>): String = list.joinToString()
|
||||
|
||||
@TypeConverter
|
||||
fun stringToStringList(string: String): List<String> = string.split(", ")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,34 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
}
|
||||
|
||||
group = "com.meloda.app.fast.datastore"
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.datastore"
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Configs.minSdk
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = Configs.java
|
||||
targetCompatibility = Configs.java
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Configs.java.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.common)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.meloda.app.fast.datastore
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.PowerManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
|
||||
fun isUsingDarkMode(
|
||||
resources: Resources,
|
||||
powerManager: PowerManager,
|
||||
): Boolean {
|
||||
val nightThemeMode: Int = SettingsController.getInt(
|
||||
SettingsKeys.KEY_APPEARANCE_DARK_THEME,
|
||||
SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME
|
||||
)
|
||||
|
||||
val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES
|
||||
val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||
|
||||
val systemUiNightMode = resources.configuration.uiMode
|
||||
|
||||
val isSystemBatterySaver = powerManager.isPowerSaveMode
|
||||
val isSystemUsingDarkTheme =
|
||||
systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
|
||||
return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
|
||||
fun isUsingDynamicColors(): Boolean = SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_USE_DYNAMIC_COLORS,
|
||||
SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS
|
||||
)
|
||||
|
||||
fun isUsingAmoledBackground(): Boolean = SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_APPEARANCE_AMOLED_THEME,
|
||||
SettingsKeys.DEFAULT_VALUE_APPEARANCE_AMOLED_THEME
|
||||
)
|
||||
|
||||
fun selectedColorScheme(): Int = SettingsController.getInt(
|
||||
SettingsKeys.KEY_APPEARANCE_COLOR_SCHEME,
|
||||
SettingsKeys.DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME
|
||||
)
|
||||
|
||||
fun isUsingBlur(): Boolean = SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_APPEARANCE_BLUR,
|
||||
SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_BLUR
|
||||
)
|
||||
|
||||
fun isDebugSettingsShown(): Boolean = SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY,
|
||||
false
|
||||
)
|
||||
|
||||
fun isMultiline(): Boolean = SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_APPEARANCE_MULTILINE,
|
||||
SettingsKeys.DEFAULT_VALUE_MULTILINE
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.meloda.app.fast.datastore
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
object SettingsController {
|
||||
|
||||
private var preferences: SharedPreferences by Delegates.notNull()
|
||||
|
||||
fun init(preferences: SharedPreferences) {
|
||||
this.preferences = preferences
|
||||
}
|
||||
|
||||
fun edit(
|
||||
commit: Boolean = false,
|
||||
action: SharedPreferences.Editor.() -> Unit
|
||||
) {
|
||||
preferences.edit(commit, action)
|
||||
}
|
||||
|
||||
fun getString(key: String, defaultValue: String?): String? {
|
||||
return preferences.getString(key, defaultValue)
|
||||
}
|
||||
|
||||
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||
return preferences.getBoolean(key, defaultValue)
|
||||
}
|
||||
|
||||
fun getInt(key: String, defaultValue: Int): Int {
|
||||
return preferences.getInt(key, defaultValue)
|
||||
}
|
||||
|
||||
fun getLong(key: String, defaultValue: Long): Long {
|
||||
return preferences.getLong(key, defaultValue)
|
||||
}
|
||||
|
||||
fun getFloat(key: String, defaultValue: Float): Float {
|
||||
return preferences.getFloat(key, defaultValue)
|
||||
}
|
||||
|
||||
fun <T> put(key: String, newValue: T?) {
|
||||
preferences.edit {
|
||||
when (newValue) {
|
||||
is String -> putString(key, newValue)
|
||||
is Boolean -> putBoolean(key, newValue)
|
||||
is Int -> putInt(key, newValue)
|
||||
is Long -> putLong(key, newValue)
|
||||
is Float -> putFloat(key, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.meloda.app.fast.datastore
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
|
||||
object SettingsKeys {
|
||||
const val KEY_ACCOUNT = "account"
|
||||
const val KEY_ACCOUNT_LOGOUT = "account_logout"
|
||||
|
||||
const val KEY_GENERAL = "general"
|
||||
const val KEY_USE_CONTACT_NAMES = "general_use_contact_names"
|
||||
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
|
||||
const val KEY_SHOW_EMOJI_BUTTON = "general_show_emoji_button"
|
||||
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
|
||||
|
||||
const val KEY_APPEARANCE = "appearance"
|
||||
const val KEY_APPEARANCE_MULTILINE = "appearance_multiline"
|
||||
const val DEFAULT_VALUE_MULTILINE = true
|
||||
const val KEY_APPEARANCE_DARK_THEME = "appearance_appearance_dark_theme"
|
||||
const val DEFAULT_VALUE_APPEARANCE_DARK_THEME = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
const val KEY_APPEARANCE_AMOLED_THEME = "appearance_amoled_theme"
|
||||
const val DEFAULT_VALUE_APPEARANCE_AMOLED_THEME = false
|
||||
const val KEY_USE_DYNAMIC_COLORS = "appearance_use_dynamic_colors"
|
||||
const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false
|
||||
const val KEY_APPEARANCE_COLOR_SCHEME = "appearance_color_scheme"
|
||||
const val DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME = 0
|
||||
const val KEY_APPEARANCE_LANGUAGE = "appearance_language"
|
||||
const val KEY_APPEARANCE_BLUR = "appearance_blur"
|
||||
const val DEFAULT_VALUE_KEY_APPEARANCE_BLUR = false
|
||||
|
||||
const val KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL = "features_hide_keyboard_on_scroll"
|
||||
const val KEY_FEATURES_FAST_TEXT = "features_fast_text"
|
||||
const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯"
|
||||
const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background"
|
||||
const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = false
|
||||
|
||||
const val KEY_VISIBILITY_SEND_ONLINE_STATUS = "visibility_send_online_status"
|
||||
const val DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS = false
|
||||
|
||||
const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash"
|
||||
const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert"
|
||||
const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list"
|
||||
const val KEY_SHOW_EXACT_TIME_ON_TIME_STAMP = "wip_show_exact_time_on_time_stamp"
|
||||
const val KEY_SHOW_NAME_IN_BUBBLES = "debug_show_title_in_bubbles"
|
||||
const val KEY_SHOW_DATE_UNDER_BUBBLES = "debug_show_date_under_bubbles"
|
||||
const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages"
|
||||
|
||||
const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category"
|
||||
|
||||
const val ID_DMITRY = 37610580
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.meloda.app.fast.datastore
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.PowerManager
|
||||
import com.meloda.app.fast.datastore.model.ThemeConfig
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
interface UserSettings {
|
||||
val theme: StateFlow<ThemeConfig>
|
||||
val longPollBackground: StateFlow<Boolean>
|
||||
val online: StateFlow<Boolean>
|
||||
val debugSettingsEnabled: StateFlow<Boolean>
|
||||
val useContactNames: StateFlow<Boolean>
|
||||
|
||||
fun updateUsingDarkTheme()
|
||||
fun useDarkThemeChanged(use: Boolean)
|
||||
fun useAmoledThemeChanged(use: Boolean)
|
||||
fun useDynamicColorsChanged(use: Boolean)
|
||||
fun useBlurChanged(use: Boolean)
|
||||
fun useMultiline(use: Boolean)
|
||||
fun setLongPollBackground(background: Boolean)
|
||||
fun setOnline(use: Boolean)
|
||||
fun enableDebugSettings(enable: Boolean)
|
||||
fun onUseContactNamesChanged(use: Boolean)
|
||||
}
|
||||
|
||||
class UserSettingsImpl(
|
||||
private val resources: Resources,
|
||||
private val powerManager: PowerManager
|
||||
) : UserSettings {
|
||||
|
||||
override val theme = MutableStateFlow(
|
||||
ThemeConfig(
|
||||
usingDarkStyle = isUsingDarkMode(resources, powerManager),
|
||||
usingDynamicColors = isUsingDynamicColors(),
|
||||
selectedColorScheme = selectedColorScheme(),
|
||||
usingAmoledBackground = isUsingAmoledBackground(),
|
||||
usingBlur = isUsingBlur(),
|
||||
multiline = isMultiline()
|
||||
)
|
||||
)
|
||||
|
||||
override val longPollBackground = MutableStateFlow(
|
||||
SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
)
|
||||
)
|
||||
override val online = MutableStateFlow(
|
||||
SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||
SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
|
||||
)
|
||||
)
|
||||
|
||||
override val debugSettingsEnabled = MutableStateFlow(
|
||||
SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override val useContactNames = MutableStateFlow(
|
||||
SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_USE_CONTACT_NAMES,
|
||||
SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES
|
||||
)
|
||||
)
|
||||
|
||||
override fun updateUsingDarkTheme() {
|
||||
useDarkThemeChanged(
|
||||
isUsingDarkMode(
|
||||
resources = resources,
|
||||
powerManager = powerManager,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun useDarkThemeChanged(use: Boolean) {
|
||||
theme.value = theme.value.copy(
|
||||
usingDarkStyle = use
|
||||
)
|
||||
}
|
||||
|
||||
override fun useAmoledThemeChanged(use: Boolean) {
|
||||
theme.value = theme.value.copy(
|
||||
usingAmoledBackground = use
|
||||
)
|
||||
}
|
||||
|
||||
override fun useDynamicColorsChanged(use: Boolean) {
|
||||
theme.value = theme.value.copy(usingDynamicColors = use)
|
||||
}
|
||||
|
||||
override fun useBlurChanged(use: Boolean) {
|
||||
theme.value = theme.value.copy(usingBlur = use)
|
||||
}
|
||||
|
||||
override fun useMultiline(use: Boolean) {
|
||||
theme.value = theme.value.copy(multiline = use)
|
||||
}
|
||||
|
||||
override fun setLongPollBackground(background: Boolean) {
|
||||
longPollBackground.value = background
|
||||
}
|
||||
|
||||
override fun setOnline(use: Boolean) {
|
||||
online.value = use
|
||||
}
|
||||
|
||||
override fun enableDebugSettings(enable: Boolean) {
|
||||
debugSettingsEnabled.update { enable }
|
||||
}
|
||||
|
||||
override fun onUseContactNamesChanged(use: Boolean) {
|
||||
useContactNames.update { use }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.meloda.app.fast.datastore.di
|
||||
|
||||
import com.meloda.app.fast.datastore.UserSettings
|
||||
import com.meloda.app.fast.datastore.UserSettingsImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val dataStoreModule = module {
|
||||
singleOf(::UserSettingsImpl) bind UserSettings::class
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.meloda.app.fast.datastore.model
|
||||
|
||||
data class ThemeConfig(
|
||||
val usingDarkStyle: Boolean,
|
||||
val usingDynamicColors: Boolean,
|
||||
val selectedColorScheme: Int,
|
||||
val usingAmoledBackground: Boolean,
|
||||
val usingBlur: Boolean,
|
||||
val multiline: Boolean,
|
||||
val bubblesWithPinch: Boolean = true
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,51 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose.compiler)
|
||||
}
|
||||
|
||||
group = "com.meloda.app.fast.designsystem"
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.designsystem"
|
||||
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Configs.minSdk
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = Configs.java
|
||||
targetCompatibility = Configs.java
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Configs.java.toString()
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
useLiveLiterals = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// TODO: 05/05/2024, Danil Nikolaev: maybe remove
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.datastore)
|
||||
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.accompanist.permissions)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
|
||||
implementation(libs.haze)
|
||||
implementation(libs.haze.materials)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.meloda.app.fast.datastore.isUsingAmoledBackground
|
||||
import com.meloda.app.fast.datastore.isUsingDynamicColors
|
||||
import com.meloda.app.fast.datastore.model.ThemeConfig
|
||||
import com.meloda.app.fast.datastore.selectedColorScheme
|
||||
import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme
|
||||
|
||||
private val googleSansFonts = FontFamily(
|
||||
Font(resId = R.font.google_sans_regular),
|
||||
Font(
|
||||
resId = R.font.google_sans_italic,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_medium,
|
||||
weight = FontWeight.Medium
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_medium_italic,
|
||||
weight = FontWeight.Medium,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_bold,
|
||||
weight = FontWeight.Bold
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_bold_italic,
|
||||
weight = FontWeight.Bold,
|
||||
style = FontStyle.Italic
|
||||
)
|
||||
)
|
||||
|
||||
private val robotoFonts = FontFamily(
|
||||
Font(
|
||||
resId = R.font.roboto_thin,
|
||||
weight = FontWeight.Thin
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_thin_italic,
|
||||
weight = FontWeight.Thin,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_light,
|
||||
weight = FontWeight.Light
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_light_italic,
|
||||
weight = FontWeight.Light,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(resId = R.font.roboto_regular),
|
||||
Font(
|
||||
resId = R.font.roboto_italic,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_medium,
|
||||
weight = FontWeight.Medium
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_medium_italic,
|
||||
weight = FontWeight.Medium,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_bold,
|
||||
weight = FontWeight.Bold
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_bold_italic,
|
||||
weight = FontWeight.Bold,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_black,
|
||||
weight = FontWeight.Black
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_black_italic,
|
||||
weight = FontWeight.Black,
|
||||
style = FontStyle.Italic
|
||||
)
|
||||
)
|
||||
|
||||
val LocalTheme = compositionLocalOf {
|
||||
ThemeConfig(
|
||||
usingDarkStyle = false,
|
||||
usingDynamicColors = false,
|
||||
selectedColorScheme = 0,
|
||||
usingAmoledBackground = false,
|
||||
usingBlur = false,
|
||||
multiline = false
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
predefinedColorScheme: ColorScheme? = null,
|
||||
useDarkTheme: Boolean = isUsingDarkTheme(),
|
||||
useDynamicColors: Boolean = isUsingDynamicColors(),
|
||||
selectedColorScheme: Int = selectedColorScheme(),
|
||||
useAmoledBackground: Boolean = isUsingAmoledBackground(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme: ColorScheme = when {
|
||||
useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (useDarkTheme) dynamicDarkColorScheme(context)
|
||||
else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// TODO: 03/07/2024, Danil Nikolaev: add color picker to settings
|
||||
when (selectedColorScheme) {
|
||||
1 -> if (useDarkTheme) darkColorScheme() else lightColorScheme()
|
||||
else -> if (useDarkTheme) ClassicColorScheme.darkScheme else ClassicColorScheme.lightScheme
|
||||
}
|
||||
}
|
||||
}.let { scheme ->
|
||||
if (useDarkTheme && useAmoledBackground) {
|
||||
scheme.copy(
|
||||
background = Color.Black,
|
||||
surface = Color.Black
|
||||
)
|
||||
} else {
|
||||
scheme
|
||||
}
|
||||
}
|
||||
|
||||
val typography = MaterialTheme.typography.copy(
|
||||
displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = googleSansFonts),
|
||||
displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = googleSansFonts),
|
||||
displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = googleSansFonts),
|
||||
headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = googleSansFonts),
|
||||
headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = googleSansFonts),
|
||||
headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = googleSansFonts),
|
||||
bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = robotoFonts),
|
||||
bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = robotoFonts),
|
||||
bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = robotoFonts)
|
||||
)
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
insetsController.isAppearanceLightStatusBars = !useDarkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = predefinedColorScheme ?: colorScheme,
|
||||
typography = typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
import android.os.Build
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.Autofill
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Modifier.connectNode(handler: AutoFillHandler): Modifier {
|
||||
return with(handler) { fillBounds() }
|
||||
}
|
||||
|
||||
fun Modifier.defaultFocusChangeAutoFill(handler: AutoFillHandler): Modifier {
|
||||
return this.then(
|
||||
Modifier.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
handler.request()
|
||||
} else {
|
||||
handler.cancel()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun autoFillRequestHandler(
|
||||
autofillTypes: List<AutofillType> = listOf(),
|
||||
onFill: (String) -> Unit,
|
||||
): AutoFillHandler {
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
var isFillRecently = remember { false }
|
||||
val autoFillNode = remember {
|
||||
AutofillNode(
|
||||
autofillTypes = autofillTypes,
|
||||
onFill = {
|
||||
isFillRecently = true
|
||||
onFill(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autoFillNode
|
||||
return remember {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
object : AutoFillHandler {
|
||||
val autofillManager = context.getSystemService(AutofillManager::class.java)
|
||||
override fun requestManual() {
|
||||
autofillManager.requestAutofill(
|
||||
view,
|
||||
autoFillNode.id,
|
||||
autoFillNode.boundingBox?.toAndroidRect() ?: error("BoundingBox is not provided yet")
|
||||
)
|
||||
}
|
||||
|
||||
override fun requestVerifyManual() {
|
||||
if (isFillRecently) {
|
||||
isFillRecently = false
|
||||
requestManual()
|
||||
}
|
||||
}
|
||||
|
||||
override val autoFill: Autofill?
|
||||
get() = autofill
|
||||
|
||||
override val autoFillNode: AutofillNode
|
||||
get() = autoFillNode
|
||||
|
||||
override fun request() {
|
||||
autofill?.requestAutofillForNode(autofillNode = autoFillNode)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
autofill?.cancelAutofillForNode(autofillNode = autoFillNode)
|
||||
}
|
||||
|
||||
override fun Modifier.fillBounds(): Modifier {
|
||||
return this.then(
|
||||
Modifier.onGloballyPositioned {
|
||||
autoFillNode.boundingBox = it.boundsInWindow()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Rect.toAndroidRect(): android.graphics.Rect {
|
||||
return android.graphics.Rect(
|
||||
left.roundToInt(),
|
||||
top.roundToInt(),
|
||||
right.roundToInt(),
|
||||
bottom.roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
interface AutoFillHandler {
|
||||
|
||||
val autoFill: Autofill?
|
||||
val autoFillNode: AutofillNode
|
||||
fun requestVerifyManual()
|
||||
fun requestManual()
|
||||
fun request()
|
||||
fun cancel()
|
||||
fun Modifier.fillBounds(): Modifier
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
|
||||
/**
|
||||
* Default alpha levels used by Material components.
|
||||
*
|
||||
* See [LocalContentAlpha].
|
||||
*/
|
||||
object ContentAlpha {
|
||||
/**
|
||||
* A high level of content alpha, used to represent high emphasis text such as input text in a
|
||||
* selected [TextField].
|
||||
*/
|
||||
val high: Float
|
||||
@Composable
|
||||
get() = contentAlpha(
|
||||
highContrastAlpha = HighContrastContentAlpha.high,
|
||||
lowContrastAlpha = LowContrastContentAlpha.high
|
||||
)
|
||||
|
||||
/**
|
||||
* A medium level of content alpha, used to represent medium emphasis text such as
|
||||
* placeholder text in a [TextField].
|
||||
*/
|
||||
val medium: Float
|
||||
@Composable
|
||||
get() = contentAlpha(
|
||||
highContrastAlpha = HighContrastContentAlpha.medium,
|
||||
lowContrastAlpha = LowContrastContentAlpha.medium
|
||||
)
|
||||
|
||||
/**
|
||||
* A low level of content alpha used to represent disabled components, such as text in a
|
||||
* disabled [Button].
|
||||
*/
|
||||
val disabled: Float
|
||||
@Composable
|
||||
get() = contentAlpha(
|
||||
highContrastAlpha = HighContrastContentAlpha.disabled,
|
||||
lowContrastAlpha = LowContrastContentAlpha.disabled
|
||||
)
|
||||
|
||||
/**
|
||||
* This default implementation uses separate alpha levels depending on the luminance of the
|
||||
* incoming color, and whether the theme is light or dark. This is to ensure correct contrast
|
||||
* and accessibility on all surfaces.
|
||||
*
|
||||
* See [HighContrastContentAlpha] and [LowContrastContentAlpha] for what the levels are
|
||||
* used for, and under what circumstances.
|
||||
*/
|
||||
@Composable
|
||||
private fun contentAlpha(
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
highContrastAlpha: Float,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
lowContrastAlpha: Float
|
||||
): Float {
|
||||
val contentColor = LocalContentColor.current
|
||||
return if (!isUsingDarkTheme()) {
|
||||
if (contentColor.luminance() > 0.5) highContrastAlpha else lowContrastAlpha
|
||||
} else {
|
||||
if (contentColor.luminance() < 0.5) highContrastAlpha else lowContrastAlpha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CompositionLocal containing the preferred content alpha for a given position in the hierarchy.
|
||||
* This alpha is used for text and iconography ([Text] and [Icon]) to emphasize / de-emphasize
|
||||
* different parts of a component. See the Material guide on
|
||||
* [Text Legibility](https://material.io/design/color/text-legibility.html) for more information on
|
||||
* alpha levels used by text and iconography.
|
||||
*
|
||||
* See [ContentAlpha] for the default levels used by most Material components.
|
||||
*
|
||||
* [MaterialTheme] sets this to [ContentAlpha.high] by default, as this is the default alpha for
|
||||
* body text.
|
||||
*
|
||||
* @sample androidx.compose.material.samples.ContentAlphaSample
|
||||
*/
|
||||
val LocalContentAlpha = compositionLocalOf { 1f }
|
||||
|
||||
/**
|
||||
* Alpha levels for high luminance content in light theme, or low luminance content in dark theme.
|
||||
*
|
||||
* This content will typically be placed on colored surfaces, so it is important that the
|
||||
* contrast here is higher to meet accessibility standards, and increase legibility.
|
||||
*
|
||||
* These levels are typically used for text / iconography in primary colored tabs /
|
||||
* bottom navigation / etc.
|
||||
*/
|
||||
private object HighContrastContentAlpha {
|
||||
const val high: Float = 1.00f
|
||||
const val medium: Float = 0.74f
|
||||
const val disabled: Float = 0.38f
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha levels for low luminance content in light theme, or high luminance content in dark theme.
|
||||
*
|
||||
* This content will typically be placed on grayscale surfaces, so the contrast here can be lower
|
||||
* without sacrificing accessibility and legibility.
|
||||
*
|
||||
* These levels are typically used for body text on the main surface (white in light theme, grey
|
||||
* in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc.
|
||||
*/
|
||||
private object LowContrastContentAlpha {
|
||||
const val high: Float = 0.87f
|
||||
const val medium: Float = 0.60f
|
||||
const val disabled: Float = 0.38f
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.common.util.AndroidUtils
|
||||
import com.meloda.app.fast.datastore.SettingsController
|
||||
import com.meloda.app.fast.datastore.SettingsKeys
|
||||
|
||||
@Composable
|
||||
fun isUsingDarkTheme(): Boolean {
|
||||
val nightThemeMode = SettingsController.getInt(
|
||||
SettingsKeys.KEY_APPEARANCE_DARK_THEME,
|
||||
SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME
|
||||
)
|
||||
val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES
|
||||
val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val systemUiNightMode = context.resources.configuration.uiMode
|
||||
|
||||
val isSystemBatterySaver = AndroidUtils.isBatterySaverOn(context)
|
||||
val isSystemUsingDarkTheme =
|
||||
systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
|
||||
return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UiText?.getString(): String? {
|
||||
return when (this) {
|
||||
is UiText.Resource -> {
|
||||
stringResource(id = resId)
|
||||
}
|
||||
|
||||
is UiText.ResourceParams -> {
|
||||
val processedArgs = args.map { any ->
|
||||
when (any) {
|
||||
is UiText -> any.getString().orEmpty()
|
||||
else -> any.toString()
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
stringResource(id = value, *processedArgs)
|
||||
}
|
||||
|
||||
is UiText.QuantityResource -> {
|
||||
pluralStringResource(id = resId, count = quantity, quantity)
|
||||
}
|
||||
|
||||
is UiText.Simple -> text
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.handleTabKey(
|
||||
action: () -> Boolean
|
||||
): Modifier = this.onKeyEvent { event ->
|
||||
if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB) {
|
||||
action.invoke()
|
||||
} else false
|
||||
}
|
||||
|
||||
fun Modifier.handleEnterKey(
|
||||
action: () -> Boolean
|
||||
): Modifier = this.onKeyEvent { event ->
|
||||
if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
action.invoke()
|
||||
} else false
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun CheckPermission(
|
||||
showRationale: @Composable () -> Unit,
|
||||
onDenied: @Composable () -> Unit,
|
||||
permission: PermissionState,
|
||||
) {
|
||||
when (val status = permission.status) {
|
||||
is PermissionStatus.Denied -> {
|
||||
if (status.shouldShowRationale) {
|
||||
showRationale()
|
||||
} else {
|
||||
onDenied()
|
||||
}
|
||||
}
|
||||
|
||||
is PermissionStatus.Granted -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun RequestPermission(
|
||||
permission: PermissionState
|
||||
) {
|
||||
LaunchedEffect(Unit) { permission.launchPermissionRequest() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
class ImmutableList<T>(val values: List<T>) : Iterable<T> {
|
||||
|
||||
constructor(size: Int, init: (index: Int) -> T) : this(MutableList(size, init))
|
||||
|
||||
operator fun get(index: Int): T? {
|
||||
values.singleOrNull()
|
||||
return values[index]
|
||||
}
|
||||
|
||||
inline fun forEach(action: (T) -> Unit) {
|
||||
for (element in values) action(element)
|
||||
}
|
||||
|
||||
inline fun <R> map(transform: (T) -> R): ImmutableList<R> {
|
||||
return values.map(transform).toImmutableList()
|
||||
}
|
||||
|
||||
inline fun <R> mapIndexed(transform: (index: Int, T) -> R): ImmutableList<R> {
|
||||
return values.mapIndexed(transform).toImmutableList()
|
||||
}
|
||||
|
||||
fun singleOrNull(): T? {
|
||||
return if (values.size == 1) this[0] else null
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean = values.isEmpty()
|
||||
|
||||
fun isNotEmpty(): Boolean = !isEmpty()
|
||||
|
||||
inline fun singleOrNull(predicate: (T) -> Boolean): T? {
|
||||
var single: T? = null
|
||||
var found = false
|
||||
for (element in this) {
|
||||
if (predicate(element)) {
|
||||
if (found) return null
|
||||
single = element
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if (!found) return null
|
||||
return single
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <T> copyOf(collection: Collection<T>): ImmutableList<T> =
|
||||
ImmutableList(collection.toList())
|
||||
|
||||
fun <T> List<T>.toImmutableList(): ImmutableList<T> = ImmutableList(this)
|
||||
|
||||
fun <T> empty(): ImmutableList<T> = ImmutableList(emptyList())
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> = values.listIterator()
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
fun LocalContentAlpha(
|
||||
defaultColor: Color = MaterialTheme.colorScheme.onBackground,
|
||||
alpha: Float,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides defaultColor.copy(alpha = alpha)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList
|
||||
|
||||
// TODO: 08.04.2023, Danil Nikolaev: review
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MaterialDialog(
|
||||
onDismissAction: (() -> Unit),
|
||||
title: UiText? = null,
|
||||
text: UiText? = null,
|
||||
confirmText: UiText? = null,
|
||||
confirmAction: (() -> Unit)? = null,
|
||||
cancelText: UiText? = null,
|
||||
cancelAction: (() -> Unit)? = null,
|
||||
neutralText: UiText? = null,
|
||||
neutralAction: (() -> Unit)? = null,
|
||||
itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None,
|
||||
preSelectedItems: ImmutableList<Int> = ImmutableList.empty(),
|
||||
items: ImmutableList<UiText> = ImmutableList.empty(),
|
||||
onItemClick: ((index: Int) -> Unit)? = null,
|
||||
buttonsInvokeDismiss: Boolean = true,
|
||||
customContent: (@Composable ColumnScope.() -> Unit)? = null,
|
||||
) {
|
||||
var isVisible by remember {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
val onDismissRequest = {
|
||||
onDismissAction.invoke()
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
val stringTitles = items.map { it.getString().orEmpty() }
|
||||
|
||||
var alertItems by remember {
|
||||
mutableStateOf(
|
||||
stringTitles.mapIndexed { index, title ->
|
||||
DialogItem(
|
||||
title,
|
||||
preSelectedItems.contains(index)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AppTheme {
|
||||
if (isVisible) {
|
||||
// AlertAnimation(visible = isVisible) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = onDismissRequest
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } }
|
||||
val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } }
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
shape = AlertDialogDefaults.shape,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||
) {
|
||||
Column(modifier = Modifier.padding(bottom = 10.dp)) {
|
||||
val stringTitle = title?.getString()
|
||||
if (stringTitle != null) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
Row {
|
||||
stringTitle?.let { title ->
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (canScrollBackward) {
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val stringMessage = text?.getString()
|
||||
if (stringMessage != null && stringTitle == null) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
Row {
|
||||
stringMessage?.let { message ->
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (alertItems.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
AlertItems(
|
||||
selectionType = itemsSelectionType,
|
||||
items = alertItems,
|
||||
onItemClick = { index ->
|
||||
onItemClick?.invoke(index)
|
||||
|
||||
if (itemsSelectionType == ItemsSelectionType.None) {
|
||||
onDismissRequest.invoke()
|
||||
} else {
|
||||
val newItems =
|
||||
alertItems.mapIndexed { itemIndex, item ->
|
||||
item.copy(isSelected = itemIndex == index)
|
||||
}
|
||||
|
||||
alertItems = newItems
|
||||
}
|
||||
},
|
||||
onItemCheckedChanged = { index ->
|
||||
val newItems = alertItems.toMutableList()
|
||||
val oldItem = newItems[index]
|
||||
newItems[index] =
|
||||
oldItem.copy(isSelected = !oldItem.isSelected)
|
||||
|
||||
alertItems = newItems.toImmutableList()
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
} else {
|
||||
customContent?.let { content ->
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
content.invoke(this)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canScrollForward) {
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
neutralText?.getString()?.let { text ->
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (buttonsInvokeDismiss) {
|
||||
onDismissRequest.invoke()
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
neutralAction?.invoke()
|
||||
}
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
cancelText?.getString()?.let { text ->
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (buttonsInvokeDismiss) {
|
||||
onDismissRequest.invoke()
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
cancelAction?.invoke()
|
||||
}
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
|
||||
confirmText?.getString()?.let { text ->
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (buttonsInvokeDismiss) {
|
||||
onDismissRequest.invoke()
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
confirmAction?.invoke()
|
||||
}
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AlertAnimation(
|
||||
visible: Boolean,
|
||||
content: @Composable AnimatedVisibilityScope.() -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(animationSpec = tween(400)) +
|
||||
scaleIn(animationSpec = tween(400)),
|
||||
exit = fadeOut(animationSpec = tween(150)),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AlertItemsPreview() {
|
||||
AppTheme {
|
||||
AlertItems(
|
||||
selectionType = ItemsSelectionType.None,
|
||||
items = ImmutableList(5) { index ->
|
||||
DialogItem(
|
||||
title = "Item #${index + 1}",
|
||||
isSelected = index % 2 == 0
|
||||
)
|
||||
},
|
||||
onItemClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AlertItems(
|
||||
selectionType: ItemsSelectionType,
|
||||
items: ImmutableList<DialogItem>,
|
||||
onItemClick: ((index: Int) -> Unit)? = null,
|
||||
onItemCheckedChanged: ((index: Int) -> Unit)? = null
|
||||
) {
|
||||
items.forEachIndexed { index, item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clickable {
|
||||
if (selectionType == ItemsSelectionType.Multi) {
|
||||
onItemCheckedChanged?.invoke(index)
|
||||
} else {
|
||||
onItemClick?.invoke(index)
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// TODO: 29/12/2023, Danil Nikolaev: check onClick & onCheckedChange actions
|
||||
when (selectionType) {
|
||||
ItemsSelectionType.Multi -> {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Checkbox(
|
||||
checked = item.isSelected,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
}
|
||||
|
||||
ItemsSelectionType.Single -> {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
RadioButton(
|
||||
selected = item.isSelected,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
ItemsSelectionType.None -> {
|
||||
Spacer(modifier = Modifier.width(26.dp))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DialogItem(
|
||||
val title: String,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
sealed interface ItemsSelectionType {
|
||||
data object Single : ItemsSelectionType
|
||||
data object Multi : ItemsSelectionType
|
||||
data object None : ItemsSelectionType
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
data class TabItem(
|
||||
val titleResId: Int?,
|
||||
val unselectedIconResId: Int?,
|
||||
val selectedIconResId: Int?
|
||||
)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.meloda.app.fast.designsystem
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TextFieldErrorText(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String = stringResource(id = R.string.error_empty_field),
|
||||
withSpacer: Boolean = true
|
||||
) {
|
||||
Row {
|
||||
if (withSpacer) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package com.meloda.app.fast.designsystem.colorschemes
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
object ClassicColorScheme {
|
||||
private val primaryLight = Color(0xFF405F90)
|
||||
private val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
private val primaryContainerLight = Color(0xFFD6E3FF)
|
||||
private val onPrimaryContainerLight = Color(0xFF001B3D)
|
||||
private val secondaryLight = Color(0xFF555F71)
|
||||
private val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
private val secondaryContainerLight = Color(0xFFDAE2F9)
|
||||
private val onSecondaryContainerLight = Color(0xFF121C2B)
|
||||
private val tertiaryLight = Color(0xFF6F5575)
|
||||
private val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
private val tertiaryContainerLight = Color(0xFFF9D8FD)
|
||||
private val onTertiaryContainerLight = Color(0xFF28132F)
|
||||
private val errorLight = Color(0xFFBA1A1A)
|
||||
private val onErrorLight = Color(0xFFFFFFFF)
|
||||
private val errorContainerLight = Color(0xFFFFDAD6)
|
||||
private val onErrorContainerLight = Color(0xFF410002)
|
||||
private val backgroundLight = Color(0xFFF9F9FF)
|
||||
private val onBackgroundLight = Color(0xFF191C20)
|
||||
private val surfaceLight = Color(0xFFF9F9FF)
|
||||
private val onSurfaceLight = Color(0xFF191C20)
|
||||
private val surfaceVariantLight = Color(0xFFE0E2EC)
|
||||
private val onSurfaceVariantLight = Color(0xFF44474E)
|
||||
private val outlineLight = Color(0xFF74777F)
|
||||
private val outlineVariantLight = Color(0xFFC4C6CF)
|
||||
private val scrimLight = Color(0xFF000000)
|
||||
private val inverseSurfaceLight = Color(0xFF2E3036)
|
||||
private val inverseOnSurfaceLight = Color(0xFFF0F0F7)
|
||||
private val inversePrimaryLight = Color(0xFFA9C7FF)
|
||||
private val surfaceDimLight = Color(0xFFD9D9E0)
|
||||
private val surfaceBrightLight = Color(0xFFF9F9FF)
|
||||
private val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
private val surfaceContainerLowLight = Color(0xFFF3F3FA)
|
||||
private val surfaceContainerLight = Color(0xFFEDEDF4)
|
||||
private val surfaceContainerHighLight = Color(0xFFE7E8EE)
|
||||
private val surfaceContainerHighestLight = Color(0xFFE2E2E9)
|
||||
|
||||
private val primaryDark = Color(0xFFA9C7FF)
|
||||
private val onPrimaryDark = Color(0xFF08305F)
|
||||
private val primaryContainerDark = Color(0xFF274777)
|
||||
private val onPrimaryContainerDark = Color(0xFFD6E3FF)
|
||||
private val secondaryDark = Color(0xFFBDC7DC)
|
||||
private val onSecondaryDark = Color(0xFF283141)
|
||||
private val secondaryContainerDark = Color(0xFF3E4758)
|
||||
private val onSecondaryContainerDark = Color(0xFFDAE2F9)
|
||||
private val tertiaryDark = Color(0xFFDCBCE1)
|
||||
private val onTertiaryDark = Color(0xFF3F2845)
|
||||
private val tertiaryContainerDark = Color(0xFF563E5C)
|
||||
private val onTertiaryContainerDark = Color(0xFFF9D8FD)
|
||||
private val errorDark = Color(0xFFFFB4AB)
|
||||
private val onErrorDark = Color(0xFF690005)
|
||||
private val errorContainerDark = Color(0xFF93000A)
|
||||
private val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
private val backgroundDark = Color(0xFF111318)
|
||||
private val onBackgroundDark = Color(0xFFE2E2E9)
|
||||
private val surfaceDark = Color(0xFF111318)
|
||||
private val onSurfaceDark = Color(0xFFE2E2E9)
|
||||
private val surfaceVariantDark = Color(0xFF44474E)
|
||||
private val onSurfaceVariantDark = Color(0xFFC4C6CF)
|
||||
private val outlineDark = Color(0xFF8E9099)
|
||||
private val outlineVariantDark = Color(0xFF44474E)
|
||||
private val scrimDark = Color(0xFF000000)
|
||||
private val inverseSurfaceDark = Color(0xFFE2E2E9)
|
||||
private val inverseOnSurfaceDark = Color(0xFF2E3036)
|
||||
private val inversePrimaryDark = Color(0xFF405F90)
|
||||
private val surfaceDimDark = Color(0xFF111318)
|
||||
private val surfaceBrightDark = Color(0xFF37393E)
|
||||
private val surfaceContainerLowestDark = Color(0xFF0C0E13)
|
||||
private val surfaceContainerLowDark = Color(0xFF191C20)
|
||||
private val surfaceContainerDark = Color(0xFF1D2024)
|
||||
private val surfaceContainerHighDark = Color(0xFF282A2F)
|
||||
private val surfaceContainerHighestDark = Color(0xFF33353A)
|
||||
|
||||
val darkScheme = darkColorScheme(
|
||||
primary = primaryDark,
|
||||
onPrimary = onPrimaryDark,
|
||||
primaryContainer = primaryContainerDark,
|
||||
onPrimaryContainer = onPrimaryContainerDark,
|
||||
secondary = secondaryDark,
|
||||
onSecondary = onSecondaryDark,
|
||||
secondaryContainer = secondaryContainerDark,
|
||||
onSecondaryContainer = onSecondaryContainerDark,
|
||||
tertiary = tertiaryDark,
|
||||
onTertiary = onTertiaryDark,
|
||||
tertiaryContainer = tertiaryContainerDark,
|
||||
onTertiaryContainer = onTertiaryContainerDark,
|
||||
error = errorDark,
|
||||
onError = onErrorDark,
|
||||
errorContainer = errorContainerDark,
|
||||
onErrorContainer = onErrorContainerDark,
|
||||
background = backgroundDark,
|
||||
onBackground = onBackgroundDark,
|
||||
surface = surfaceDark,
|
||||
onSurface = onSurfaceDark,
|
||||
surfaceVariant = surfaceVariantDark,
|
||||
onSurfaceVariant = onSurfaceVariantDark,
|
||||
outline = outlineDark,
|
||||
outlineVariant = outlineVariantDark,
|
||||
scrim = scrimDark,
|
||||
inverseSurface = inverseSurfaceDark,
|
||||
inverseOnSurface = inverseOnSurfaceDark,
|
||||
inversePrimary = inversePrimaryDark,
|
||||
surfaceDim = surfaceDimDark,
|
||||
surfaceBright = surfaceBrightDark,
|
||||
surfaceContainerLowest = surfaceContainerLowestDark,
|
||||
surfaceContainerLow = surfaceContainerLowDark,
|
||||
surfaceContainer = surfaceContainerDark,
|
||||
surfaceContainerHigh = surfaceContainerHighDark,
|
||||
surfaceContainerHighest = surfaceContainerHighestDark,
|
||||
)
|
||||
|
||||
val lightScheme = lightColorScheme(
|
||||
primary = primaryLight,
|
||||
onPrimary = onPrimaryLight,
|
||||
primaryContainer = primaryContainerLight,
|
||||
onPrimaryContainer = onPrimaryContainerLight,
|
||||
secondary = secondaryLight,
|
||||
onSecondary = onSecondaryLight,
|
||||
secondaryContainer = secondaryContainerLight,
|
||||
onSecondaryContainer = onSecondaryContainerLight,
|
||||
tertiary = tertiaryLight,
|
||||
onTertiary = onTertiaryLight,
|
||||
tertiaryContainer = tertiaryContainerLight,
|
||||
onTertiaryContainer = onTertiaryContainerLight,
|
||||
error = errorLight,
|
||||
onError = onErrorLight,
|
||||
errorContainer = errorContainerLight,
|
||||
onErrorContainer = onErrorContainerLight,
|
||||
background = backgroundLight,
|
||||
onBackground = onBackgroundLight,
|
||||
surface = surfaceLight,
|
||||
onSurface = onSurfaceLight,
|
||||
surfaceVariant = surfaceVariantLight,
|
||||
onSurfaceVariant = onSurfaceVariantLight,
|
||||
outline = outlineLight,
|
||||
outlineVariant = outlineVariantLight,
|
||||
scrim = scrimLight,
|
||||
inverseSurface = inverseSurfaceLight,
|
||||
inverseOnSurface = inverseOnSurfaceLight,
|
||||
inversePrimary = inversePrimaryLight,
|
||||
surfaceDim = surfaceDimLight,
|
||||
surfaceBright = surfaceBrightLight,
|
||||
surfaceContainerLowest = surfaceContainerLowestLight,
|
||||
surfaceContainerLow = surfaceContainerLowLight,
|
||||
surfaceContainer = surfaceContainerLight,
|
||||
surfaceContainerHigh = surfaceContainerHighLight,
|
||||
surfaceContainerHighest = surfaceContainerHighestLight,
|
||||
)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.meloda.app.fast.designsystem.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.meloda.app.fast.designsystem.LocalTheme
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalHazeMaterialsApi::class
|
||||
)
|
||||
@Composable
|
||||
fun BlurrableTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
listState: LazyListState?,
|
||||
hazeState: HazeState = remember { HazeState() }
|
||||
) {
|
||||
val currentTheme = LocalTheme.current
|
||||
|
||||
val toolbarColorAlpha by animateFloatAsState(
|
||||
targetValue = if (listState == null || !listState.canScrollBackward) 1f else 0f,
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
)
|
||||
|
||||
val toolbarContainerColor by animateColorAsState(
|
||||
targetValue =
|
||||
if (currentTheme.usingBlur || listState != null && !listState.canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
)
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = toolbarContainerColor.copy(
|
||||
alpha = if (currentTheme.usingBlur) toolbarColorAlpha else 1f
|
||||
)
|
||||
),
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (currentTheme.usingBlur) {
|
||||
Modifier.hazeChild(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.meloda.app.fast.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun FullScreenLoader(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.meloda.app.fast.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.meloda.app.fast.designsystem.R
|
||||
|
||||
@Composable
|
||||
fun NoItemsView(
|
||||
modifier: Modifier = Modifier,
|
||||
customText: String? = null
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = customText ?: stringResource(id = R.string.no_items),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user