forked from melod1n/fast-messenger
upstream changes
upstream changes
This commit is contained in:
+112
-51
@@ -1,5 +1,7 @@
|
|||||||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||||
|
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||||
|
|
||||||
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage", "\"\"")
|
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage", "\"\"")
|
||||||
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint", "\"\"")
|
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint", "\"\"")
|
||||||
@@ -17,13 +19,14 @@ plugins {
|
|||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("kotlin-kapt")
|
id("kotlin-kapt")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("dagger.hilt.android.plugin")
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.meloda.fast"
|
namespace = "com.meloda.fast"
|
||||||
|
|
||||||
compileSdk = 32
|
compileSdk = 34
|
||||||
|
|
||||||
applicationVariants.all {
|
applicationVariants.all {
|
||||||
outputs.all {
|
outputs.all {
|
||||||
@@ -34,14 +37,14 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.meloda.fast"
|
applicationId = "com.meloda.fast"
|
||||||
minSdk = 23
|
minSdk = 24
|
||||||
targetSdk = 32
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "alpha"
|
versionName = "alpha"
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
annotationProcessorOptions {
|
annotationProcessorOptions {
|
||||||
arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
|
// arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,18 +76,51 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val flavorDimension = "version"
|
||||||
|
|
||||||
|
flavorDimensions += flavorDimension
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
create("dev") {
|
||||||
|
resourceConfigurations += listOf("en", "xxhdpi")
|
||||||
|
|
||||||
|
dimension = flavorDimension
|
||||||
|
applicationIdSuffix = ".dev"
|
||||||
|
versionNameSuffix = "-dev"
|
||||||
|
}
|
||||||
|
create("full") {
|
||||||
|
dimension = flavorDimension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=compatibility", "-opt-in=kotlin.RequiresOptIn")
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.4.5"
|
||||||
|
useLiveLiterals = true
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
|
fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
|
||||||
@@ -92,74 +128,99 @@ fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
|
|||||||
val currentTime get() = (System.currentTimeMillis() / 1000).toInt()
|
val currentTime get() = (System.currentTimeMillis() / 1000).toInt()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("reflect", "1.6.10"))
|
|
||||||
|
|
||||||
implementation(libs.androidx.core)
|
|
||||||
|
|
||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
// DI zone
|
||||||
implementation(libs.androidx.lifecycle.livedata)
|
implementation("io.insert-koin:koin-android:3.4.0")
|
||||||
implementation(libs.androidx.lifecycle.runtime)
|
// end of DI zone
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
|
|
||||||
implementation(libs.androidx.lifecycle.common.java8)
|
|
||||||
|
|
||||||
implementation(libs.androidx.splashScreen)
|
implementation("com.github.skydoves:cloudy:0.1.2")
|
||||||
|
|
||||||
implementation(libs.androidx.dataStore)
|
implementation("io.coil-kt:coil-compose:2.3.0")
|
||||||
|
implementation("io.coil-kt:coil:2.3.0")
|
||||||
|
|
||||||
implementation(libs.androidx.appCompat)
|
implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2")
|
||||||
|
implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2")
|
||||||
|
|
||||||
implementation(libs.androidx.activity)
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.21")
|
||||||
|
|
||||||
implementation(libs.androidx.fragment)
|
implementation("androidx.core:core-ktx:1.10.1")
|
||||||
|
|
||||||
implementation(libs.androidx.preference)
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
||||||
|
|
||||||
implementation(libs.androidx.swipeRefreshLayout)
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
|
|
||||||
implementation(libs.androidx.recyclerView)
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
|
||||||
implementation(libs.androidx.cardView)
|
implementation("androidx.activity:activity-ktx:1.7.2")
|
||||||
|
|
||||||
implementation(libs.androidx.constraintLayout)
|
implementation("androidx.fragment:fragment-ktx:1.6.1")
|
||||||
|
|
||||||
implementation(libs.androidx.room)
|
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||||
implementation(libs.androidx.room.runtime)
|
|
||||||
kapt(libs.androidx.room.compiler)
|
|
||||||
|
|
||||||
implementation(libs.cicerone)
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
implementation(libs.waveformSeekBar)
|
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||||
|
|
||||||
implementation(libs.glide)
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
kapt(libs.glide.compiler)
|
|
||||||
|
|
||||||
implementation(libs.kPermissions)
|
implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
|
||||||
implementation(libs.kPermissions.coroutines)
|
|
||||||
|
|
||||||
implementation(libs.appCenter.analytics)
|
implementation("androidx.room:room-ktx:2.5.2")
|
||||||
implementation(libs.appCenter.crashes)
|
implementation("androidx.room:room-runtime:2.5.2")
|
||||||
|
ksp("androidx.room:room-compiler:2.5.2")
|
||||||
|
|
||||||
implementation(libs.hilt)
|
implementation("com.github.terrakok:cicerone:7.1")
|
||||||
kapt(libs.hilt.compiler)
|
|
||||||
|
|
||||||
implementation(libs.retrofit)
|
implementation("com.github.massoudss:waveformSeekBar:5.0.0")
|
||||||
implementation(libs.retrofit.gson.converter)
|
|
||||||
|
|
||||||
implementation(libs.okhttp3)
|
implementation("com.github.bumptech.glide:glide:4.15.1")
|
||||||
implementation(libs.okhttp3.interceptor)
|
ksp("com.github.bumptech.glide:compiler:4.15.1")
|
||||||
|
|
||||||
implementation(libs.coroutines.core)
|
implementation("com.github.fondesa:kpermissions:3.4.0")
|
||||||
implementation(libs.coroutines.android)
|
implementation("com.github.fondesa:kpermissions-coroutines:3.4.0")
|
||||||
|
|
||||||
implementation(libs.viewBindingDelegate)
|
implementation("com.microsoft.appcenter:appcenter-analytics:5.0.1")
|
||||||
|
implementation("com.microsoft.appcenter:appcenter-crashes:5.0.1")
|
||||||
|
|
||||||
implementation(libs.google.gson)
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
|
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||||
|
|
||||||
implementation(libs.google.guava)
|
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11")
|
||||||
|
|
||||||
implementation(libs.google.material)
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.7.1")
|
||||||
|
|
||||||
implementation(libs.jsoup)
|
implementation("com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.9")
|
||||||
|
|
||||||
implementation(libs.chucker)
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
|
|
||||||
|
implementation("com.google.guava:guava:31.1-jre")
|
||||||
|
|
||||||
|
implementation("com.google.android.material:material:1.9.0")
|
||||||
|
|
||||||
|
implementation("com.github.chuckerteam.chucker:library:3.5.2")
|
||||||
|
|
||||||
|
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
|
||||||
|
|
||||||
|
// Compose zone
|
||||||
|
implementation(platform("androidx.compose:compose-bom:2023.04.01"))
|
||||||
|
|
||||||
|
implementation("androidx.compose.material3:material3:1.1.1")
|
||||||
|
// implementation("androidx.compose.material:material:1.4.3")
|
||||||
|
implementation("androidx.compose.ui:ui:1.4.3")
|
||||||
|
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
|
||||||
|
|
||||||
|
implementation("androidx.compose.material3:material3-window-size-class:1.1.1")
|
||||||
|
|
||||||
|
implementation("androidx.activity:activity-compose:1.7.2")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
|
||||||
|
|
||||||
|
implementation("androidx.compose.runtime:runtime-saveable:1.6.0-alpha02")
|
||||||
|
// end of Compose zone
|
||||||
}
|
}
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 33,
|
|
||||||
"identityHash": "ab075cc511743c47de441d484159b088",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "accounts",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, PRIMARY KEY(`userId`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "userId",
|
|
||||||
"columnName": "userId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "accessToken",
|
|
||||||
"columnName": "accessToken",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "fastToken",
|
|
||||||
"columnName": "fastToken",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "conversations",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo200` TEXT, `type` TEXT NOT NULL, `callInProgress` INTEGER NOT NULL, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `isMarkedUnread` INTEGER NOT NULL, `lastMessageId` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessage_id` INTEGER, `pinnedMessage_text` TEXT, `pinnedMessage_isOut` INTEGER, `pinnedMessage_peerId` INTEGER, `pinnedMessage_fromId` INTEGER, `pinnedMessage_date` INTEGER, `pinnedMessage_randomId` INTEGER, `pinnedMessage_action` TEXT, `pinnedMessage_actionMemberId` INTEGER, `pinnedMessage_actionText` TEXT, `pinnedMessage_actionConversationMessageId` INTEGER, `pinnedMessage_actionMessage` TEXT, `pinnedMessage_important` INTEGER, `pinnedMessage_forwards` TEXT, `pinnedMessage_attachments` TEXT, `pinnedMessage_replyMessage` TEXT, `pinnedMessage_geo` TEXT, `lastMessage_id` INTEGER, `lastMessage_text` TEXT, `lastMessage_isOut` INTEGER, `lastMessage_peerId` INTEGER, `lastMessage_fromId` INTEGER, `lastMessage_date` INTEGER, `lastMessage_randomId` INTEGER, `lastMessage_action` TEXT, `lastMessage_actionMemberId` INTEGER, `lastMessage_actionText` TEXT, `lastMessage_actionConversationMessageId` INTEGER, `lastMessage_actionMessage` TEXT, `lastMessage_important` INTEGER, `lastMessage_forwards` TEXT, `lastMessage_attachments` TEXT, `lastMessage_replyMessage` TEXT, `lastMessage_geo` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ownerId",
|
|
||||||
"columnName": "ownerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "callInProgress",
|
|
||||||
"columnName": "callInProgress",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPhantom",
|
|
||||||
"columnName": "isPhantom",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastConversationMessageId",
|
|
||||||
"columnName": "lastConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "inRead",
|
|
||||||
"columnName": "inRead",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "outRead",
|
|
||||||
"columnName": "outRead",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMarkedUnread",
|
|
||||||
"columnName": "isMarkedUnread",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessageId",
|
|
||||||
"columnName": "lastMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "unreadCount",
|
|
||||||
"columnName": "unreadCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "membersCount",
|
|
||||||
"columnName": "membersCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canChangePin",
|
|
||||||
"columnName": "canChangePin",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "majorId",
|
|
||||||
"columnName": "majorId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "minorId",
|
|
||||||
"columnName": "minorId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.id",
|
|
||||||
"columnName": "pinnedMessage_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.text",
|
|
||||||
"columnName": "pinnedMessage_text",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.isOut",
|
|
||||||
"columnName": "pinnedMessage_isOut",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.peerId",
|
|
||||||
"columnName": "pinnedMessage_peerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.fromId",
|
|
||||||
"columnName": "pinnedMessage_fromId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.date",
|
|
||||||
"columnName": "pinnedMessage_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.randomId",
|
|
||||||
"columnName": "pinnedMessage_randomId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.action",
|
|
||||||
"columnName": "pinnedMessage_action",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionMemberId",
|
|
||||||
"columnName": "pinnedMessage_actionMemberId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionText",
|
|
||||||
"columnName": "pinnedMessage_actionText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionConversationMessageId",
|
|
||||||
"columnName": "pinnedMessage_actionConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionMessage",
|
|
||||||
"columnName": "pinnedMessage_actionMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.important",
|
|
||||||
"columnName": "pinnedMessage_important",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.forwards",
|
|
||||||
"columnName": "pinnedMessage_forwards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.attachments",
|
|
||||||
"columnName": "pinnedMessage_attachments",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.replyMessage",
|
|
||||||
"columnName": "pinnedMessage_replyMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.geo",
|
|
||||||
"columnName": "pinnedMessage_geo",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.id",
|
|
||||||
"columnName": "lastMessage_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.text",
|
|
||||||
"columnName": "lastMessage_text",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.isOut",
|
|
||||||
"columnName": "lastMessage_isOut",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.peerId",
|
|
||||||
"columnName": "lastMessage_peerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.fromId",
|
|
||||||
"columnName": "lastMessage_fromId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.date",
|
|
||||||
"columnName": "lastMessage_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.randomId",
|
|
||||||
"columnName": "lastMessage_randomId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.action",
|
|
||||||
"columnName": "lastMessage_action",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionMemberId",
|
|
||||||
"columnName": "lastMessage_actionMemberId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionText",
|
|
||||||
"columnName": "lastMessage_actionText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionConversationMessageId",
|
|
||||||
"columnName": "lastMessage_actionConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionMessage",
|
|
||||||
"columnName": "lastMessage_actionMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.important",
|
|
||||||
"columnName": "lastMessage_important",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.forwards",
|
|
||||||
"columnName": "lastMessage_forwards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.attachments",
|
|
||||||
"columnName": "lastMessage_attachments",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.replyMessage",
|
|
||||||
"columnName": "lastMessage_replyMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.geo",
|
|
||||||
"columnName": "lastMessage_geo",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "messages",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `important` INTEGER NOT NULL, `forwards` TEXT, `attachments` TEXT, `replyMessage` TEXT, `geo` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "text",
|
|
||||||
"columnName": "text",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isOut",
|
|
||||||
"columnName": "isOut",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "peerId",
|
|
||||||
"columnName": "peerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "fromId",
|
|
||||||
"columnName": "fromId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "date",
|
|
||||||
"columnName": "date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "randomId",
|
|
||||||
"columnName": "randomId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "action",
|
|
||||||
"columnName": "action",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionMemberId",
|
|
||||||
"columnName": "actionMemberId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionText",
|
|
||||||
"columnName": "actionText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionConversationMessageId",
|
|
||||||
"columnName": "actionConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionMessage",
|
|
||||||
"columnName": "actionMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "important",
|
|
||||||
"columnName": "important",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "forwards",
|
|
||||||
"columnName": "forwards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "attachments",
|
|
||||||
"columnName": "attachments",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "replyMessage",
|
|
||||||
"columnName": "replyMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "geo",
|
|
||||||
"columnName": "geo",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "users",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `online` INTEGER NOT NULL, `photo200` TEXT, `lastSeen` INTEGER, `lastSeenStatus` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "firstName",
|
|
||||||
"columnName": "firstName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastName",
|
|
||||||
"columnName": "lastName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "online",
|
|
||||||
"columnName": "online",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastSeen",
|
|
||||||
"columnName": "lastSeen",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastSeenStatus",
|
|
||||||
"columnName": "lastSeenStatus",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "groups",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "screenName",
|
|
||||||
"columnName": "screenName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "membersCount",
|
|
||||||
"columnName": "membersCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ab075cc511743c47de441d484159b088')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,600 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 34,
|
|
||||||
"identityHash": "2c202b1fce1b5f6c6ab0da756e0590a6",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "accounts",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, PRIMARY KEY(`userId`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "userId",
|
|
||||||
"columnName": "userId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "accessToken",
|
|
||||||
"columnName": "accessToken",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "fastToken",
|
|
||||||
"columnName": "fastToken",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "conversations",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo200` TEXT, `type` TEXT NOT NULL, `callInProgress` INTEGER NOT NULL, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `isMarkedUnread` INTEGER NOT NULL, `lastMessageId` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessage_id` INTEGER, `pinnedMessage_text` TEXT, `pinnedMessage_isOut` INTEGER, `pinnedMessage_peerId` INTEGER, `pinnedMessage_fromId` INTEGER, `pinnedMessage_date` INTEGER, `pinnedMessage_randomId` INTEGER, `pinnedMessage_action` TEXT, `pinnedMessage_actionMemberId` INTEGER, `pinnedMessage_actionText` TEXT, `pinnedMessage_actionConversationMessageId` INTEGER, `pinnedMessage_actionMessage` TEXT, `pinnedMessage_updateTime` INTEGER, `pinnedMessage_important` INTEGER, `pinnedMessage_forwards` TEXT, `pinnedMessage_attachments` TEXT, `pinnedMessage_replyMessage` TEXT, `pinnedMessage_geo` TEXT, `lastMessage_id` INTEGER, `lastMessage_text` TEXT, `lastMessage_isOut` INTEGER, `lastMessage_peerId` INTEGER, `lastMessage_fromId` INTEGER, `lastMessage_date` INTEGER, `lastMessage_randomId` INTEGER, `lastMessage_action` TEXT, `lastMessage_actionMemberId` INTEGER, `lastMessage_actionText` TEXT, `lastMessage_actionConversationMessageId` INTEGER, `lastMessage_actionMessage` TEXT, `lastMessage_updateTime` INTEGER, `lastMessage_important` INTEGER, `lastMessage_forwards` TEXT, `lastMessage_attachments` TEXT, `lastMessage_replyMessage` TEXT, `lastMessage_geo` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ownerId",
|
|
||||||
"columnName": "ownerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "callInProgress",
|
|
||||||
"columnName": "callInProgress",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPhantom",
|
|
||||||
"columnName": "isPhantom",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastConversationMessageId",
|
|
||||||
"columnName": "lastConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "inRead",
|
|
||||||
"columnName": "inRead",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "outRead",
|
|
||||||
"columnName": "outRead",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMarkedUnread",
|
|
||||||
"columnName": "isMarkedUnread",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessageId",
|
|
||||||
"columnName": "lastMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "unreadCount",
|
|
||||||
"columnName": "unreadCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "membersCount",
|
|
||||||
"columnName": "membersCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canChangePin",
|
|
||||||
"columnName": "canChangePin",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "majorId",
|
|
||||||
"columnName": "majorId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "minorId",
|
|
||||||
"columnName": "minorId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.id",
|
|
||||||
"columnName": "pinnedMessage_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.text",
|
|
||||||
"columnName": "pinnedMessage_text",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.isOut",
|
|
||||||
"columnName": "pinnedMessage_isOut",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.peerId",
|
|
||||||
"columnName": "pinnedMessage_peerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.fromId",
|
|
||||||
"columnName": "pinnedMessage_fromId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.date",
|
|
||||||
"columnName": "pinnedMessage_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.randomId",
|
|
||||||
"columnName": "pinnedMessage_randomId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.action",
|
|
||||||
"columnName": "pinnedMessage_action",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionMemberId",
|
|
||||||
"columnName": "pinnedMessage_actionMemberId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionText",
|
|
||||||
"columnName": "pinnedMessage_actionText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionConversationMessageId",
|
|
||||||
"columnName": "pinnedMessage_actionConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.actionMessage",
|
|
||||||
"columnName": "pinnedMessage_actionMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.updateTime",
|
|
||||||
"columnName": "pinnedMessage_updateTime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.important",
|
|
||||||
"columnName": "pinnedMessage_important",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.forwards",
|
|
||||||
"columnName": "pinnedMessage_forwards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.attachments",
|
|
||||||
"columnName": "pinnedMessage_attachments",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.replyMessage",
|
|
||||||
"columnName": "pinnedMessage_replyMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessage.geo",
|
|
||||||
"columnName": "pinnedMessage_geo",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.id",
|
|
||||||
"columnName": "lastMessage_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.text",
|
|
||||||
"columnName": "lastMessage_text",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.isOut",
|
|
||||||
"columnName": "lastMessage_isOut",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.peerId",
|
|
||||||
"columnName": "lastMessage_peerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.fromId",
|
|
||||||
"columnName": "lastMessage_fromId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.date",
|
|
||||||
"columnName": "lastMessage_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.randomId",
|
|
||||||
"columnName": "lastMessage_randomId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.action",
|
|
||||||
"columnName": "lastMessage_action",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionMemberId",
|
|
||||||
"columnName": "lastMessage_actionMemberId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionText",
|
|
||||||
"columnName": "lastMessage_actionText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionConversationMessageId",
|
|
||||||
"columnName": "lastMessage_actionConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.actionMessage",
|
|
||||||
"columnName": "lastMessage_actionMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.updateTime",
|
|
||||||
"columnName": "lastMessage_updateTime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.important",
|
|
||||||
"columnName": "lastMessage_important",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.forwards",
|
|
||||||
"columnName": "lastMessage_forwards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.attachments",
|
|
||||||
"columnName": "lastMessage_attachments",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.replyMessage",
|
|
||||||
"columnName": "lastMessage_replyMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessage.geo",
|
|
||||||
"columnName": "lastMessage_geo",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "messages",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwards` TEXT, `attachments` TEXT, `replyMessage` TEXT, `geo` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "text",
|
|
||||||
"columnName": "text",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isOut",
|
|
||||||
"columnName": "isOut",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "peerId",
|
|
||||||
"columnName": "peerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "fromId",
|
|
||||||
"columnName": "fromId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "date",
|
|
||||||
"columnName": "date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "randomId",
|
|
||||||
"columnName": "randomId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "action",
|
|
||||||
"columnName": "action",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionMemberId",
|
|
||||||
"columnName": "actionMemberId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionText",
|
|
||||||
"columnName": "actionText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionConversationMessageId",
|
|
||||||
"columnName": "actionConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionMessage",
|
|
||||||
"columnName": "actionMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "updateTime",
|
|
||||||
"columnName": "updateTime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "important",
|
|
||||||
"columnName": "important",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "forwards",
|
|
||||||
"columnName": "forwards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "attachments",
|
|
||||||
"columnName": "attachments",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "replyMessage",
|
|
||||||
"columnName": "replyMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "geo",
|
|
||||||
"columnName": "geo",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "users",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `online` INTEGER NOT NULL, `photo200` TEXT, `lastSeen` INTEGER, `lastSeenStatus` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "firstName",
|
|
||||||
"columnName": "firstName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastName",
|
|
||||||
"columnName": "lastName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "online",
|
|
||||||
"columnName": "online",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastSeen",
|
|
||||||
"columnName": "lastSeen",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastSeenStatus",
|
|
||||||
"columnName": "lastSeenStatus",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "groups",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "screenName",
|
|
||||||
"columnName": "screenName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "membersCount",
|
|
||||||
"columnName": "membersCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2c202b1fce1b5f6c6ab0da756e0590a6')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#5B37DD</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">Fast Dev</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@@ -3,36 +3,35 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".common.AppGlobal"
|
android:name=".common.AppGlobal"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:extractNativeLibs="false"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="false"
|
||||||
android:testOnly="false"
|
android:testOnly="false"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:ignore="DataExtractionRules"
|
||||||
tools:replace="android:allowBackup"
|
tools:replace="android:allowBackup"
|
||||||
tools:ignore="DataExtractionRules">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".screens.main.MainActivity"
|
android:name=".screens.main.activity.MainActivity"
|
||||||
android:theme="@style/AppTheme.Splash"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -40,15 +39,64 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".screens.testing.TestActivity" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.LongPollService"
|
android:name=".service.LongPollService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.OnlineService"
|
android:name=".service.OnlineService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.LongPollQSTileService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:icon="@drawable/ic_round_settings_24"
|
||||||
|
android:label="Open settings"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.MyCustomControlService"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="Fast"
|
||||||
|
android:permission="android.permission.BIND_CONTROLS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.controls.ControlsProviderService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data
|
||||||
|
android:name="autoStoreLocales"
|
||||||
|
android:value="true" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.StopLongPollServiceReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.meloda.fast.receiver.ACTION_STOP" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
@@ -60,5 +108,4 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -7,8 +7,6 @@ enum class ApiEvent(val value: Int) {
|
|||||||
MessageEdit(5),
|
MessageEdit(5),
|
||||||
MessageReadIncoming(6),
|
MessageReadIncoming(6),
|
||||||
MessageReadOutgoing(7),
|
MessageReadOutgoing(7),
|
||||||
FriendOnline(8),
|
|
||||||
FriendOffline(9),
|
|
||||||
MessagesDeleted(13),
|
MessagesDeleted(13),
|
||||||
PinUnpinConversation(20),
|
PinUnpinConversation(20),
|
||||||
PrivateTyping(61),
|
PrivateTyping(61),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.meloda.fast.api
|
package com.meloda.fast.api
|
||||||
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
import com.meloda.fast.common.AppGlobal
|
import com.meloda.fast.common.AppGlobal
|
||||||
import com.meloda.fast.model.AppAccount
|
import com.meloda.fast.model.AppAccount
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
object UserConfig {
|
object UserConfig {
|
||||||
|
|
||||||
@@ -42,6 +42,6 @@ object UserConfig {
|
|||||||
return currentUserId > 0 && userId > 0 && accessToken.isNotBlank()
|
return currentUserId > 0 && userId > 0 && accessToken.isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
val vkUser = MutableLiveData<VkUser?>(null)
|
val vkUser: MutableStateFlow<VkUser?> = MutableStateFlow(null)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,11 @@ object VKConstants {
|
|||||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||||
|
|
||||||
const val USER_FIELDS =
|
const val USER_FIELDS =
|
||||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
|
"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 ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
|
||||||
|
|
||||||
const val API_VERSION = "5.189"
|
const val API_VERSION = "5.173"
|
||||||
const val LP_VERSION = 10
|
const val LP_VERSION = 10
|
||||||
|
|
||||||
const val VK_APP_ID = "2274003"
|
const val VK_APP_ID = "2274003"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ open class ApiError(
|
|||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
@SerializedName("error_msg", alternate = ["error_description"])
|
@SerializedName("error_msg", alternate = ["error_description"])
|
||||||
open val errorMessage: String? = null,
|
open val errorMessage: String? = null,
|
||||||
|
@SerializedName("error_type")
|
||||||
|
val errorType: String? = null,
|
||||||
val throwable: Throwable? = null
|
val throwable: Throwable? = null
|
||||||
) : IOException() {
|
) : IOException() {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.meloda.fast.api.base
|
||||||
|
|
||||||
|
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||||
|
import okio.IOException
|
||||||
|
|
||||||
|
class AttachmentClassNameIsEmptyException(attachment: VkAttachment) :
|
||||||
|
IOException(
|
||||||
|
"attachment ${attachment.javaClass.name} does not have declared field \"className\""
|
||||||
|
)
|
||||||
@@ -9,12 +9,26 @@ sealed class LongPollEvent {
|
|||||||
data class VkMessageNewEvent(
|
data class VkMessageNewEvent(
|
||||||
val message: VkMessage,
|
val message: VkMessage,
|
||||||
val profiles: HashMap<Int, VkUser>,
|
val profiles: HashMap<Int, VkUser>,
|
||||||
val groups: HashMap<Int, VkGroup>
|
val groups: HashMap<Int, VkGroup>,
|
||||||
) : LongPollEvent()
|
) : LongPollEvent()
|
||||||
|
|
||||||
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent()
|
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent()
|
||||||
|
|
||||||
data class VkMessageReadIncomingEvent(val peerId: Int, val messageId: Int) : LongPollEvent()
|
data class VkMessageReadIncomingEvent(
|
||||||
data class VkMessageReadOutgoingEvent(val peerId: Int, val messageId: Int) : LongPollEvent()
|
val peerId: Int,
|
||||||
|
val messageId: Int,
|
||||||
|
val unreadCount: Int,
|
||||||
|
) : LongPollEvent()
|
||||||
|
|
||||||
|
data class VkMessageReadOutgoingEvent(
|
||||||
|
val peerId: Int,
|
||||||
|
val messageId: Int,
|
||||||
|
val unreadCount: Int,
|
||||||
|
) : LongPollEvent()
|
||||||
|
|
||||||
|
data class VkConversationPinStateChangedEvent(
|
||||||
|
val peerId: Int,
|
||||||
|
val majorId: Int,
|
||||||
|
) : LongPollEvent()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,12 @@ import com.meloda.fast.api.network.ApiAnswer
|
|||||||
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
|
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
|
||||||
import com.meloda.fast.base.viewmodel.VkEventCallback
|
import com.meloda.fast.base.viewmodel.VkEventCallback
|
||||||
import com.meloda.fast.data.messages.MessagesRepository
|
import com.meloda.fast.data.messages.MessagesRepository
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
@@ -47,10 +52,8 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
|
ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
|
||||||
ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
|
ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
|
||||||
ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
|
ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
|
||||||
ApiEvent.FriendOnline -> parseFriendOnline(eventType, event)
|
|
||||||
ApiEvent.FriendOffline -> parseFriendOffline(eventType, event)
|
|
||||||
ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
|
ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
|
||||||
ApiEvent.PinUnpinConversation -> onNewEvent(eventType, event)
|
ApiEvent.PinUnpinConversation -> parseConversationPinStateChanged(eventType, event)
|
||||||
ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
|
ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
|
||||||
ApiEvent.ChatTyping -> onNewEvent(eventType, event)
|
ApiEvent.ChatTyping -> onNewEvent(eventType, event)
|
||||||
ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
|
ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
|
||||||
@@ -67,6 +70,27 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
|
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseConversationPinStateChanged(eventType: ApiEvent, event: JsonArray) {
|
||||||
|
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val peerId = event[1].asInt
|
||||||
|
val majorId = event[2].asInt
|
||||||
|
|
||||||
|
launch {
|
||||||
|
listenersMap[ApiEvent.PinUnpinConversation]?.let { listeners ->
|
||||||
|
listeners.forEach { vkEventCallback ->
|
||||||
|
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>)
|
||||||
|
.onEvent(
|
||||||
|
LongPollEvent.VkConversationPinStateChangedEvent(
|
||||||
|
peerId = peerId,
|
||||||
|
majorId = majorId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
|
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||||
}
|
}
|
||||||
@@ -119,6 +143,7 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||||
val peerId = event[1].asInt
|
val peerId = event[1].asInt
|
||||||
val messageId = event[2].asInt
|
val messageId = event[2].asInt
|
||||||
|
val unreadCount = event[3].asInt
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
|
listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
|
||||||
@@ -127,7 +152,8 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
.onEvent(
|
.onEvent(
|
||||||
LongPollEvent.VkMessageReadIncomingEvent(
|
LongPollEvent.VkMessageReadIncomingEvent(
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
messageId = messageId
|
messageId = messageId,
|
||||||
|
unreadCount = unreadCount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -139,6 +165,7 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||||
val peerId = event[1].asInt
|
val peerId = event[1].asInt
|
||||||
val messageId = event[2].asInt
|
val messageId = event[2].asInt
|
||||||
|
val unreadCount = event[3].asInt
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
|
listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
|
||||||
@@ -147,7 +174,8 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
.onEvent(
|
.onEvent(
|
||||||
LongPollEvent.VkMessageReadOutgoingEvent(
|
LongPollEvent.VkMessageReadOutgoingEvent(
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
messageId = messageId
|
messageId = messageId,
|
||||||
|
unreadCount = unreadCount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -155,21 +183,13 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseFriendOnline(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseFriendOffline(eventType: ApiEvent, event: JsonArray) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
|
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
|
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
suspendCoroutine<T> {
|
suspendCoroutine {
|
||||||
launch {
|
launch {
|
||||||
val normalMessageResponse = messagesRepository.getById(
|
val normalMessageResponse = messagesRepository.getById(
|
||||||
MessagesGetByIdRequest(
|
MessagesGetByIdRequest(
|
||||||
@@ -179,7 +199,7 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!normalMessageResponse.isSuccessful()) {
|
if (normalMessageResponse.isError()) {
|
||||||
normalMessageResponse.error.throwable?.run { throw this }
|
normalMessageResponse.error.throwable?.run { throw this }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +215,12 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
|
|
||||||
val profiles = hashMapOf<Int, VkUser>()
|
val profiles = hashMapOf<Int, VkUser>()
|
||||||
messagesResponse.profiles?.forEach { baseUser ->
|
messagesResponse.profiles?.forEach { baseUser ->
|
||||||
baseUser.asVkUser().let { user -> profiles[user.id] = user }
|
baseUser.mapToDomain().let { user -> profiles[user.id] = user }
|
||||||
}
|
}
|
||||||
|
|
||||||
val groups = hashMapOf<Int, VkGroup>()
|
val groups = hashMapOf<Int, VkGroup>()
|
||||||
messagesResponse.groups?.forEach { baseGroup ->
|
messagesResponse.groups?.forEach { baseGroup ->
|
||||||
baseGroup.asVkGroup().let { group -> groups[group.id] = group }
|
baseGroup.mapToDomain().let { group -> groups[group.id] = group }
|
||||||
}
|
}
|
||||||
|
|
||||||
val resumeValue: LongPollEvent? = when (eventType) {
|
val resumeValue: LongPollEvent? = when (eventType) {
|
||||||
@@ -228,6 +248,14 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) {
|
||||||
|
registerListener(ApiEvent.PinUnpinConversation, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) {
|
||||||
|
onConversationPinStateChanged(assembleEventCallback(block))
|
||||||
|
}
|
||||||
|
|
||||||
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
|
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
|
||||||
registerListener(ApiEvent.MessageReadIncoming, listener)
|
registerListener(ApiEvent.MessageReadIncoming, listener)
|
||||||
}
|
}
|
||||||
@@ -265,8 +293,8 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal inline fun <R : Any> assembleEventCallback(crossinline block: (R) -> Unit): VkEventCallback<R> {
|
internal inline fun <R : Any> assembleEventCallback(
|
||||||
return object : VkEventCallback<R> {
|
crossinline block: (R) -> Unit,
|
||||||
override fun onEvent(event: R) = block.invoke(event)
|
): VkEventCallback<R> {
|
||||||
}
|
return VkEventCallback { event -> block.invoke(event) }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.meloda.fast.api.model
|
||||||
|
|
||||||
|
sealed class ActionState {
|
||||||
|
object Phantom : ActionState()
|
||||||
|
object CallInProgress : ActionState()
|
||||||
|
object None : ActionState()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(isPhantom: Boolean, isCallInProgress: Boolean): ActionState {
|
||||||
|
return when {
|
||||||
|
isPhantom -> Phantom
|
||||||
|
isCallInProgress -> CallInProgress
|
||||||
|
else -> None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.meloda.fast.api.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
sealed class ConversationPeerType : Parcelable {
|
||||||
|
object User : ConversationPeerType()
|
||||||
|
object Group : ConversationPeerType()
|
||||||
|
object Chat : ConversationPeerType()
|
||||||
|
|
||||||
|
fun isUser() = this == User
|
||||||
|
fun isGroup() = this == Group
|
||||||
|
fun isChat() = this == Chat
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(type: String): ConversationPeerType {
|
||||||
|
return when (type) {
|
||||||
|
"user" -> User
|
||||||
|
"group" -> Group
|
||||||
|
else -> Chat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.meloda.fast.api.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class VkChat(
|
||||||
|
val type: String,
|
||||||
|
val title: String,
|
||||||
|
val adminId: Int,
|
||||||
|
val membersCount: Int,
|
||||||
|
val id: Int,
|
||||||
|
val members: List<ChatMember> = emptyList(),
|
||||||
|
val photo50: String,
|
||||||
|
val photo100: String,
|
||||||
|
val photo200: String,
|
||||||
|
val isDefaultPhoto: Boolean
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ChatMember(
|
||||||
|
val id: Int,
|
||||||
|
val type: ChatMemberType,
|
||||||
|
val isOnline: Boolean?,
|
||||||
|
val lastSeen: Int?,
|
||||||
|
val name: String?,
|
||||||
|
val firstName: String?,
|
||||||
|
val lastName: String?,
|
||||||
|
val invitedBy: Int,
|
||||||
|
val photo50: String?,
|
||||||
|
val photo100: String?,
|
||||||
|
val photo200: String?,
|
||||||
|
val isOwner: Boolean,
|
||||||
|
val isAdmin: Boolean,
|
||||||
|
val canKick: Boolean
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun isProfile(): Boolean = type == ChatMemberType.Profile
|
||||||
|
|
||||||
|
fun isGroup(): Boolean = type == ChatMemberType.Group
|
||||||
|
|
||||||
|
enum class ChatMemberType(val value: String) {
|
||||||
|
Profile("profile"), Group("group");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(value: String) = values().first { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.meloda.fast.api.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class VkChatMember(
|
||||||
|
val memberId: Int,
|
||||||
|
val invitedBy: Int,
|
||||||
|
val joinDate: Int,
|
||||||
|
val isAdmin: Boolean,
|
||||||
|
val isOwner: Boolean,
|
||||||
|
val canKick: Boolean
|
||||||
|
) : Parcelable
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.meloda.fast.api.model
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.room.Embedded
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.Ignore
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import com.meloda.fast.api.UserConfig
|
|
||||||
import com.meloda.fast.model.SelectableItem
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Entity(tableName = "conversations")
|
|
||||||
@Parcelize
|
|
||||||
data class VkConversation(
|
|
||||||
@PrimaryKey(autoGenerate = false)
|
|
||||||
var id: Int,
|
|
||||||
var ownerId: Int?,
|
|
||||||
var title: String?,
|
|
||||||
var photo200: String?,
|
|
||||||
var type: String,
|
|
||||||
var callInProgress: Boolean,
|
|
||||||
var isPhantom: Boolean,
|
|
||||||
var lastConversationMessageId: Int,
|
|
||||||
var inRead: Int,
|
|
||||||
var outRead: Int,
|
|
||||||
var isMarkedUnread: Boolean,
|
|
||||||
var lastMessageId: Int,
|
|
||||||
var unreadCount: Int,
|
|
||||||
var membersCount: Int?,
|
|
||||||
var canChangePin: Boolean,
|
|
||||||
var majorId: Int,
|
|
||||||
var minorId: Int,
|
|
||||||
|
|
||||||
@Embedded(prefix = "pinnedMessage_")
|
|
||||||
var pinnedMessage: VkMessage? = null,
|
|
||||||
|
|
||||||
@Embedded(prefix = "lastMessage_")
|
|
||||||
var lastMessage: VkMessage? = null,
|
|
||||||
) : SelectableItem(id) {
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val user = MutableLiveData<VkUser?>()
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val group = MutableLiveData<VkGroup?>()
|
|
||||||
|
|
||||||
fun isChat() = type == "chat"
|
|
||||||
fun isUser() = type == "user"
|
|
||||||
fun isGroup() = type == "group"
|
|
||||||
|
|
||||||
fun isInUnread() = inRead - lastMessageId < 0
|
|
||||||
fun isOutUnread() = outRead - lastMessageId < 0
|
|
||||||
|
|
||||||
fun isUnread() = isInUnread() || isOutUnread()
|
|
||||||
|
|
||||||
fun isAccount() = id == UserConfig.userId
|
|
||||||
|
|
||||||
fun isPinned() = majorId > 0
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -7,11 +7,13 @@ import com.meloda.fast.api.UserConfig
|
|||||||
import com.meloda.fast.api.VKConstants
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||||
|
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||||
import com.meloda.fast.model.SelectableItem
|
import com.meloda.fast.model.SelectableItem
|
||||||
import com.meloda.fast.util.TimeUtils
|
import com.meloda.fast.util.TimeUtils
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
// TODO: 05.08.2023, Danil Nikolaev: create other class for storing in database
|
||||||
@Entity(tableName = "messages")
|
@Entity(tableName = "messages")
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkMessage constructor(
|
data class VkMessage constructor(
|
||||||
@@ -38,7 +40,7 @@ data class VkMessage constructor(
|
|||||||
var replyMessage: VkMessage? = null,
|
var replyMessage: VkMessage? = null,
|
||||||
|
|
||||||
val geo: BaseVkMessage.Geo? = null,
|
val geo: BaseVkMessage.Geo? = null,
|
||||||
) : SelectableItem(id) {
|
) : SelectableItem() {
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
@@ -48,6 +50,14 @@ data class VkMessage constructor(
|
|||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
var group: VkGroup? = null
|
var group: VkGroup? = null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var actionUser: VkUser? = null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var actionGroup: VkGroup? = null
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
var state: State = State.Sent
|
var state: State = State.Sent
|
||||||
@@ -58,7 +68,7 @@ data class VkMessage constructor(
|
|||||||
|
|
||||||
fun isGroup() = fromId < 0
|
fun isGroup() = fromId < 0
|
||||||
|
|
||||||
fun isRead(conversation: VkConversation) =
|
fun isRead(conversation: VkConversationDomain) =
|
||||||
if (isOut) {
|
if (isOut) {
|
||||||
conversation.outRead - id >= 0
|
conversation.outRead - id >= 0
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ data class VkUser(
|
|||||||
val online: Boolean,
|
val online: Boolean,
|
||||||
val photo200: String?,
|
val photo200: String?,
|
||||||
val lastSeen: Int?,
|
val lastSeen: Int?,
|
||||||
val lastSeenStatus: String?
|
val lastSeenStatus: String?,
|
||||||
|
val birthday: String?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
override fun toString() = fullName
|
override fun toString() = fullName
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.meloda.fast.model.DataItem
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class VkAttachment : DataItem<Int>(), Parcelable {
|
open class VkAttachment : Parcelable {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val dataItemId: Int = -1
|
|
||||||
|
|
||||||
open fun asString(withAccessKey: Boolean = true) = ""
|
open fun asString(withAccessKey: Boolean = true) = ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkCurator(
|
data class VkCurator(
|
||||||
val id: Int
|
val id: Int,
|
||||||
) : VkAttachment()
|
) : VkAttachment() {
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val className: String = this::class.java.name
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -14,4 +15,7 @@ data class VkStory(
|
|||||||
|
|
||||||
fun isFromGroup() = ownerId < 0
|
fun isFromGroup() = ownerId < 0
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val className: String = this::class.java.name
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ data class VkVideo(
|
|||||||
val images: List<VideoImage>,
|
val images: List<VideoImage>,
|
||||||
val firstFrames: List<BaseVkVideo.FirstFrame>?,
|
val firstFrames: List<BaseVkVideo.FirstFrame>?,
|
||||||
val accessKey: String?,
|
val accessKey: String?,
|
||||||
val title: String
|
val title: String,
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
@@ -47,11 +47,11 @@ data class VkVideo(
|
|||||||
val width: Int,
|
val width: Int,
|
||||||
val height: Int,
|
val height: Int,
|
||||||
val url: String,
|
val url: String,
|
||||||
val withPadding: Boolean
|
val withPadding: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
var shapeKind: ShapeKind
|
var shapeKind: ShapeKind? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val ratio = width.toFloat() / height.toFloat()
|
val ratio = width.toFloat() / height.toFloat()
|
||||||
@@ -64,10 +64,21 @@ data class VkVideo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class ShapeKind {
|
open class ShapeKind(val value: Int) {
|
||||||
object Vertical : ShapeKind()
|
object Square : ShapeKind(0)
|
||||||
object Horizontal : ShapeKind()
|
object Vertical : ShapeKind(1)
|
||||||
object Square : ShapeKind()
|
object Horizontal : ShapeKind(2)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
|
||||||
|
fun parse(value: Int) = when (value) {
|
||||||
|
0 -> Square
|
||||||
|
1 -> Vertical
|
||||||
|
2 -> Horizontal
|
||||||
|
else -> throw IllegalArgumentException("Unknown value: $value")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkWidget(
|
data class VkWidget(
|
||||||
val id: Int
|
val id: Int
|
||||||
) : VkAttachment()
|
) : VkAttachment() {
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val className: String = this::class.java.name
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.meloda.fast.api.model.base
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.meloda.fast.api.model.VkChat
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class BaseVkChat(
|
||||||
|
val type: String,
|
||||||
|
val title: String,
|
||||||
|
val admin_id: Int,
|
||||||
|
val members_count: Int,
|
||||||
|
val id: Int,
|
||||||
|
val photo_50: String,
|
||||||
|
val photo_100: String,
|
||||||
|
val photo_200: String,
|
||||||
|
val is_default_photo: Boolean,
|
||||||
|
val push_settings: PushSettings
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun asVkChat() = VkChat(
|
||||||
|
type = type,
|
||||||
|
title = title,
|
||||||
|
adminId = admin_id,
|
||||||
|
membersCount = members_count,
|
||||||
|
id = id,
|
||||||
|
photo50 = photo_50,
|
||||||
|
photo100 = photo_100,
|
||||||
|
photo200 = photo_200,
|
||||||
|
isDefaultPhoto = is_default_photo
|
||||||
|
)
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class PushSettings(
|
||||||
|
val sound: Int,
|
||||||
|
val disabled_until: Int
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.meloda.fast.api.model.base
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.meloda.fast.api.model.VkChatMember
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class BaseVkChatMember(
|
||||||
|
val member_id: Int,
|
||||||
|
val invited_by: Int,
|
||||||
|
val join_date: Int,
|
||||||
|
val is_admin: Boolean?,
|
||||||
|
val is_owner: Boolean?,
|
||||||
|
val can_kick: Boolean?
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun asVkChatMember() = VkChatMember(
|
||||||
|
memberId = member_id,
|
||||||
|
invitedBy = invited_by,
|
||||||
|
joinDate = join_date,
|
||||||
|
isAdmin = is_admin == true,
|
||||||
|
isOwner = is_owner == true,
|
||||||
|
canKick = can_kick == true
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ data class BaseVkGroup(
|
|||||||
val members_count: Int?
|
val members_count: Int?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun asVkGroup() = VkGroup(
|
fun mapToDomain() = VkGroup(
|
||||||
id = -id,
|
id = -id,
|
||||||
name = name,
|
name = name,
|
||||||
screenName = screen_name,
|
screenName = screen_name,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ data class BaseVkUser(
|
|||||||
val photo_200: String?,
|
val photo_200: String?,
|
||||||
val online: Int?,
|
val online: Int?,
|
||||||
val online_info: OnlineInfo?,
|
val online_info: OnlineInfo?,
|
||||||
val screen_name: String
|
val screen_name: String,
|
||||||
|
val bdate: String?
|
||||||
//...other fields
|
//...other fields
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@@ -32,14 +33,15 @@ data class BaseVkUser(
|
|||||||
val app_id: Int?
|
val app_id: Int?
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
fun asVkUser() = VkUser(
|
fun mapToDomain() = VkUser(
|
||||||
id = id,
|
id = id,
|
||||||
firstName = first_name,
|
firstName = first_name,
|
||||||
lastName = last_name,
|
lastName = last_name,
|
||||||
online = online == 1,
|
online = online == 1,
|
||||||
photo200 = photo_200,
|
photo200 = photo_200,
|
||||||
lastSeen = online_info?.last_seen,
|
lastSeen = online_info?.last_seen,
|
||||||
lastSeenStatus = online_info?.status
|
lastSeenStatus = online_info?.status,
|
||||||
|
birthday = bdate
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-35
@@ -1,9 +1,12 @@
|
|||||||
package com.meloda.fast.api.model.base
|
package com.meloda.fast.api.model.data
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.meloda.fast.api.model.VkConversation
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
|
import com.meloda.fast.api.model.VkUser
|
||||||
|
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
|
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
|
||||||
|
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -12,6 +15,8 @@ data class BaseVkConversation(
|
|||||||
val last_message_id: Int,
|
val last_message_id: Int,
|
||||||
val in_read: Int,
|
val in_read: Int,
|
||||||
val out_read: Int,
|
val out_read: Int,
|
||||||
|
val in_read_cmid: Int,
|
||||||
|
val out_read_cmid: Int,
|
||||||
val sort_id: SortId,
|
val sort_id: SortId,
|
||||||
val last_conversation_message_id: Int,
|
val last_conversation_message_id: Int,
|
||||||
val is_marked_unread: Boolean,
|
val is_marked_unread: Boolean,
|
||||||
@@ -22,43 +27,20 @@ data class BaseVkConversation(
|
|||||||
val can_receive_money: Boolean,
|
val can_receive_money: Boolean,
|
||||||
val chat_settings: ChatSettings?,
|
val chat_settings: ChatSettings?,
|
||||||
val call_in_progress: CallInProgress?,
|
val call_in_progress: CallInProgress?,
|
||||||
val unread_count: Int?
|
val unread_count: Int?,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun asVkConversation(lastMessage: VkMessage? = null) = VkConversation(
|
|
||||||
id = peer.id,
|
|
||||||
title = chat_settings?.title,
|
|
||||||
photo200 = chat_settings?.photo?.photo_200,
|
|
||||||
type = peer.type,
|
|
||||||
callInProgress = call_in_progress != null,
|
|
||||||
isPhantom = chat_settings?.is_disappearing == true,
|
|
||||||
lastConversationMessageId = last_conversation_message_id,
|
|
||||||
inRead = in_read,
|
|
||||||
outRead = out_read,
|
|
||||||
isMarkedUnread = is_marked_unread,
|
|
||||||
lastMessageId = last_message_id,
|
|
||||||
unreadCount = unread_count ?: 0,
|
|
||||||
membersCount = chat_settings?.members_count,
|
|
||||||
ownerId = chat_settings?.owner_id,
|
|
||||||
majorId = sort_id.major_id,
|
|
||||||
minorId = sort_id.minor_id,
|
|
||||||
canChangePin = chat_settings?.acl?.can_change_pin == true
|
|
||||||
).apply {
|
|
||||||
this.lastMessage = lastMessage
|
|
||||||
this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Peer(
|
data class Peer(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val type: String,
|
val type: String,
|
||||||
val local_id: Int
|
val local_id: Int,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SortId(
|
data class SortId(
|
||||||
val major_id: Int,
|
val major_id: Int,
|
||||||
val minor_id: Int
|
val minor_id: Int,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -66,12 +48,12 @@ data class BaseVkConversation(
|
|||||||
val disabled_forever: Boolean,
|
val disabled_forever: Boolean,
|
||||||
val no_sound: Boolean,
|
val no_sound: Boolean,
|
||||||
val disabled_mentions: Boolean,
|
val disabled_mentions: Boolean,
|
||||||
val disabled_mass_mentions: Boolean
|
val disabled_mass_mentions: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CanWrite(
|
data class CanWrite(
|
||||||
val allowed: Boolean
|
val allowed: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -89,7 +71,7 @@ data class BaseVkConversation(
|
|||||||
val is_disappearing: Boolean,
|
val is_disappearing: Boolean,
|
||||||
val is_service: Boolean,
|
val is_service: Boolean,
|
||||||
val theme: String?,
|
val theme: String?,
|
||||||
val pinned_message: BaseVkMessage?
|
val pinned_message: BaseVkMessage?,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -104,7 +86,7 @@ data class BaseVkConversation(
|
|||||||
val can_copy_chat: Boolean,
|
val can_copy_chat: Boolean,
|
||||||
val can_call: Boolean,
|
val can_call: Boolean,
|
||||||
val can_use_mass_mentions: Boolean,
|
val can_use_mass_mentions: Boolean,
|
||||||
val can_change_style: Boolean
|
val can_change_style: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -112,21 +94,54 @@ data class BaseVkConversation(
|
|||||||
val photo_50: String?,
|
val photo_50: String?,
|
||||||
val photo_100: String?,
|
val photo_100: String?,
|
||||||
val photo_200: String?,
|
val photo_200: String?,
|
||||||
val is_default_photo: Boolean
|
val is_default_photo: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CallInProgress(
|
data class CallInProgress(
|
||||||
val participants: BaseVkGroupCall.Participants,
|
val participants: BaseVkGroupCall.Participants,
|
||||||
val join_link: String
|
val join_link: String,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Participants(
|
data class Participants(
|
||||||
val list: List<Int>,
|
val list: List<Int>,
|
||||||
val count: Int
|
val count: Int,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mapToDomain(
|
||||||
|
lastMessage: VkMessage? = null,
|
||||||
|
conversationUser: VkUser? = null,
|
||||||
|
conversationGroup: VkGroup? = null,
|
||||||
|
) = VkConversationDomain(
|
||||||
|
id = peer.id,
|
||||||
|
localId = peer.local_id,
|
||||||
|
conversationTitle = chat_settings?.title,
|
||||||
|
conversationPhoto = chat_settings?.photo?.photo_200,
|
||||||
|
type = peer.type,
|
||||||
|
isCallInProgress = call_in_progress != null,
|
||||||
|
isPhantom = chat_settings?.is_disappearing == true,
|
||||||
|
lastConversationMessageId = last_conversation_message_id,
|
||||||
|
inRead = in_read,
|
||||||
|
outRead = out_read,
|
||||||
|
lastMessageId = last_message_id,
|
||||||
|
unreadCount = unread_count ?: 0,
|
||||||
|
membersCount = chat_settings?.members_count,
|
||||||
|
ownerId = chat_settings?.owner_id,
|
||||||
|
majorId = sort_id.major_id,
|
||||||
|
minorId = sort_id.minor_id,
|
||||||
|
canChangePin = chat_settings?.acl?.can_change_pin == true,
|
||||||
|
canChangeInfo = chat_settings?.acl?.can_change_info == true,
|
||||||
|
pinnedMessageId = chat_settings?.pinned_message?.id,
|
||||||
|
inReadCmId = in_read_cmid,
|
||||||
|
outReadCmId = out_read_cmid,
|
||||||
|
).also {
|
||||||
|
it.lastMessage = lastMessage
|
||||||
|
it.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
|
||||||
|
it.conversationUser = conversationUser
|
||||||
|
it.conversationGroup = conversationGroup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package com.meloda.fast.api.model.domain
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.meloda.fast.R
|
||||||
|
import com.meloda.fast.api.UserConfig
|
||||||
|
import com.meloda.fast.api.VkUtils
|
||||||
|
import com.meloda.fast.api.model.ActionState
|
||||||
|
import com.meloda.fast.api.model.ConversationPeerType
|
||||||
|
import com.meloda.fast.api.model.VkGroup
|
||||||
|
import com.meloda.fast.api.model.VkMessage
|
||||||
|
import com.meloda.fast.api.model.VkUser
|
||||||
|
import com.meloda.fast.api.model.presentation.VkConversationUi
|
||||||
|
import com.meloda.fast.common.AppGlobal
|
||||||
|
import com.meloda.fast.ext.isFalse
|
||||||
|
import com.meloda.fast.ext.isTrue
|
||||||
|
import com.meloda.fast.ext.orDots
|
||||||
|
import com.meloda.fast.model.base.UiImage
|
||||||
|
import com.meloda.fast.model.base.UiText
|
||||||
|
import com.meloda.fast.model.base.parseString
|
||||||
|
import com.meloda.fast.util.TimeUtils
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
@Entity(tableName = "conversations")
|
||||||
|
@Parcelize
|
||||||
|
data class VkConversationDomain(
|
||||||
|
@PrimaryKey(autoGenerate = false)
|
||||||
|
val id: Int,
|
||||||
|
val localId: Int,
|
||||||
|
val ownerId: Int?,
|
||||||
|
val conversationTitle: String?,
|
||||||
|
val conversationPhoto: String?,
|
||||||
|
val isCallInProgress: Boolean,
|
||||||
|
val isPhantom: Boolean,
|
||||||
|
val lastConversationMessageId: Int,
|
||||||
|
val inReadCmId: Int,
|
||||||
|
val outReadCmId: Int,
|
||||||
|
val inRead: Int,
|
||||||
|
val outRead: Int,
|
||||||
|
val lastMessageId: Int,
|
||||||
|
val unreadCount: Int,
|
||||||
|
val membersCount: Int?,
|
||||||
|
val canChangePin: Boolean,
|
||||||
|
val canChangeInfo: Boolean,
|
||||||
|
val majorId: Int,
|
||||||
|
val minorId: Int,
|
||||||
|
val pinnedMessageId: Int?,
|
||||||
|
val type: String,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var peerType: ConversationPeerType = ConversationPeerType.parse(type)
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var lastMessage: VkMessage? = null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var pinnedMessage: VkMessage? = null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var conversationUser: VkUser? = null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var conversationGroup: VkGroup? = null
|
||||||
|
|
||||||
|
fun isChat() = peerType.isChat()
|
||||||
|
fun isUser() = peerType.isUser()
|
||||||
|
fun isGroup() = peerType.isGroup()
|
||||||
|
|
||||||
|
fun isInUnread() = inRead - lastMessageId < 0
|
||||||
|
fun isOutUnread() = outRead - lastMessageId < 0
|
||||||
|
|
||||||
|
fun isUnread() = isInUnread() || isOutUnread()
|
||||||
|
|
||||||
|
fun isAccount() = id == UserConfig.userId
|
||||||
|
|
||||||
|
fun isPinned() = majorId > 0
|
||||||
|
|
||||||
|
fun extractAvatar(): UiImage {
|
||||||
|
val placeholderImage = UiImage.Resource(R.drawable.ic_account_circle_cut)
|
||||||
|
|
||||||
|
val avatarLink = when {
|
||||||
|
peerType.isUser() -> {
|
||||||
|
if (id == UserConfig.userId) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
conversationUser?.photo200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerType.isGroup() -> conversationGroup?.photo200
|
||||||
|
peerType.isChat() -> conversationPhoto
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatarLink?.let(UiImage::Url) ?: placeholderImage
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractTitle(): UiText {
|
||||||
|
return when {
|
||||||
|
isAccount() -> UiText.Resource(R.string.favorites)
|
||||||
|
peerType.isChat() -> UiText.Simple(conversationTitle ?: "...")
|
||||||
|
peerType.isUser() -> UiText.Simple(conversationUser?.fullName ?: "...")
|
||||||
|
peerType.isGroup() -> UiText.Simple(conversationGroup?.name ?: "...")
|
||||||
|
else -> UiText.Simple("...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractUnreadCounterText(): String? {
|
||||||
|
if (lastMessage?.isOut.isFalse && !isInUnread()) return null
|
||||||
|
|
||||||
|
return when (unreadCount) {
|
||||||
|
in 1..999 -> unreadCount.toString()
|
||||||
|
0 -> null
|
||||||
|
else -> "%dK".format(unreadCount / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 07.01.2023, Danil Nikolaev: rewrite
|
||||||
|
fun extractMessage(): String {
|
||||||
|
val actionMessage = VkUtils.getActionConversationText(
|
||||||
|
message = lastMessage,
|
||||||
|
youPrefix = "You",
|
||||||
|
messageUser = lastMessage?.user,
|
||||||
|
messageGroup = lastMessage?.group,
|
||||||
|
action = lastMessage?.getPreparedAction(),
|
||||||
|
actionUser = lastMessage?.actionUser,
|
||||||
|
actionGroup = lastMessage?.actionGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
val attachmentIcon: UiImage? = when {
|
||||||
|
lastMessage?.text == null -> null
|
||||||
|
!lastMessage?.forwards.isNullOrEmpty() -> {
|
||||||
|
if (lastMessage?.forwards?.size == 1) {
|
||||||
|
UiImage.Resource(R.drawable.ic_attachment_forwarded_message)
|
||||||
|
} else {
|
||||||
|
UiImage.Resource(R.drawable.ic_attachment_forwarded_messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> VkUtils.getAttachmentConversationIcon(lastMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
val attachmentText = (if (attachmentIcon == null) VkUtils.getAttachmentText(
|
||||||
|
message = lastMessage
|
||||||
|
) else null)
|
||||||
|
|
||||||
|
val forwardsMessage = (if (lastMessage?.text == null) VkUtils.getForwardsText(
|
||||||
|
message = lastMessage
|
||||||
|
) else null)
|
||||||
|
|
||||||
|
val messageText = lastMessage?.text?.let(UiText::Simple)
|
||||||
|
|
||||||
|
var prefix = when {
|
||||||
|
actionMessage != null -> ""
|
||||||
|
lastMessage?.isOut.isTrue -> "You: "
|
||||||
|
else ->
|
||||||
|
when {
|
||||||
|
lastMessage?.user != null && lastMessage?.user?.firstName?.isNotBlank().isTrue -> {
|
||||||
|
"${lastMessage?.user?.firstName}: "
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMessage?.group != null && lastMessage?.group?.name?.isNotBlank().isTrue -> {
|
||||||
|
"${lastMessage?.group?.name}: "
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!peerType.isChat() && lastMessage?.isOut.isFalse) || id == UserConfig.userId)
|
||||||
|
prefix = ""
|
||||||
|
|
||||||
|
val finalText =
|
||||||
|
(actionMessage ?: forwardsMessage ?: attachmentText ?: messageText)
|
||||||
|
?.parseString(AppGlobal.Instance)
|
||||||
|
?.let(VkUtils::prepareMessageText)
|
||||||
|
?.let { text -> "$prefix$text" }
|
||||||
|
|
||||||
|
|
||||||
|
return finalText.orDots()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractAttachmentImage(): UiImage? {
|
||||||
|
if (lastMessage?.text == null) return null
|
||||||
|
return VkUtils.getAttachmentConversationIcon(lastMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractReadCondition(): Boolean {
|
||||||
|
return (lastMessage?.isOut.isTrue && isOutUnread()) ||
|
||||||
|
(lastMessage?.isOut.isFalse && isInUnread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractDate(): String {
|
||||||
|
return TimeUtils.getLocalizedTime(AppGlobal.Instance, (lastMessage?.date ?: -1) * 1000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 05.08.2023, Danil Nikolaev: rewrite
|
||||||
|
fun extractBirthday(): Boolean {
|
||||||
|
val birthday = conversationUser?.birthday ?: return false
|
||||||
|
val splitBirthday = birthday.split(".")
|
||||||
|
|
||||||
|
return if (splitBirthday.size > 1) {
|
||||||
|
val birthdayCalendar = Calendar.getInstance().apply {
|
||||||
|
this[Calendar.DAY_OF_MONTH] = splitBirthday.first().toIntOrNull() ?: -1
|
||||||
|
this[Calendar.MONTH] = (splitBirthday[1].toIntOrNull() ?: 0) - 1
|
||||||
|
}
|
||||||
|
val nowCalendar = Calendar.getInstance()
|
||||||
|
|
||||||
|
(nowCalendar[Calendar.DAY_OF_MONTH] == birthdayCalendar[Calendar.DAY_OF_MONTH]
|
||||||
|
&& nowCalendar[Calendar.MONTH] == birthdayCalendar[Calendar.MONTH])
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToPresentation() = VkConversationUi(
|
||||||
|
conversationId = id,
|
||||||
|
lastMessageId = lastMessageId,
|
||||||
|
avatar = extractAvatar(),
|
||||||
|
title = extractTitle(),
|
||||||
|
unreadCount = extractUnreadCounterText(),
|
||||||
|
date = extractDate(),
|
||||||
|
message = extractMessage(),
|
||||||
|
attachmentImage = extractAttachmentImage(),
|
||||||
|
isPinned = majorId > 0,
|
||||||
|
actionState = ActionState.parse(isPhantom, isCallInProgress),
|
||||||
|
isBirthday = extractBirthday(),
|
||||||
|
isUnread = extractReadCondition(),
|
||||||
|
isAccount = isAccount(),
|
||||||
|
isOnline = !isAccount() && conversationUser?.online == true,
|
||||||
|
lastMessage = lastMessage,
|
||||||
|
conversationUser = conversationUser,
|
||||||
|
conversationGroup = conversationGroup,
|
||||||
|
peerType = peerType
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.meloda.fast.api.model.presentation
|
||||||
|
|
||||||
|
import com.meloda.fast.api.model.ActionState
|
||||||
|
import com.meloda.fast.api.model.ConversationPeerType
|
||||||
|
import com.meloda.fast.api.model.VkGroup
|
||||||
|
import com.meloda.fast.api.model.VkMessage
|
||||||
|
import com.meloda.fast.api.model.VkUser
|
||||||
|
import com.meloda.fast.model.base.AdapterDiffItem
|
||||||
|
import com.meloda.fast.model.base.UiImage
|
||||||
|
import com.meloda.fast.model.base.UiText
|
||||||
|
|
||||||
|
data class VkConversationUi(
|
||||||
|
val conversationId: Int,
|
||||||
|
val lastMessageId: Int,
|
||||||
|
val avatar: UiImage,
|
||||||
|
val title: UiText,
|
||||||
|
val unreadCount: String?,
|
||||||
|
val date: String,
|
||||||
|
val message: String,
|
||||||
|
val attachmentImage: UiImage?,
|
||||||
|
val isPinned: Boolean,
|
||||||
|
val actionState: ActionState,
|
||||||
|
val isBirthday: Boolean,
|
||||||
|
val isUnread: Boolean,
|
||||||
|
val isAccount: Boolean,
|
||||||
|
val isOnline: Boolean,
|
||||||
|
val lastMessage: VkMessage?,
|
||||||
|
val conversationUser: VkUser?,
|
||||||
|
val conversationGroup: VkGroup?,
|
||||||
|
val peerType: ConversationPeerType,
|
||||||
|
) : AdapterDiffItem {
|
||||||
|
override val id = conversationId
|
||||||
|
}
|
||||||
@@ -43,9 +43,10 @@ object VkErrorCodes {
|
|||||||
const val InvalidDocId = 1150
|
const val InvalidDocId = 1150
|
||||||
const val InvalidDocTitle = 1152
|
const val InvalidDocTitle = 1152
|
||||||
const val AccessToDocDenied = 1153
|
const val AccessToDocDenied = 1153
|
||||||
|
|
||||||
|
const val AccessTokenExpired = 1117
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
object VkErrors {
|
object VkErrors {
|
||||||
const val Unknown = "unknown_error"
|
const val Unknown = "unknown_error"
|
||||||
|
|
||||||
@@ -55,7 +56,18 @@ object VkErrors {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorizationError : ApiError()
|
object VkErrorTypes {
|
||||||
|
const val OtpFormatIncorrect = "otp_format_is_incorrect"
|
||||||
|
const val WrongOtp = "wrong_otp"
|
||||||
|
}
|
||||||
|
|
||||||
|
object VkErrorMessages {
|
||||||
|
const val UserBanned = "user has been banned"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class AuthorizationError : ApiError()
|
||||||
|
|
||||||
|
class TokenExpiredError : AuthorizationError()
|
||||||
|
|
||||||
data class ValidationRequiredError(
|
data class ValidationRequiredError(
|
||||||
@SerializedName("validation_type")
|
@SerializedName("validation_type")
|
||||||
@@ -76,3 +88,23 @@ data class CaptchaRequiredError(
|
|||||||
@SerializedName("captcha_img")
|
@SerializedName("captcha_img")
|
||||||
val captchaImg: String
|
val captchaImg: String
|
||||||
) : ApiError()
|
) : ApiError()
|
||||||
|
|
||||||
|
object WrongTwoFaCodeFormatError : ApiError()
|
||||||
|
|
||||||
|
object WrongTwoFaCodeError : ApiError()
|
||||||
|
|
||||||
|
data class UserBannedError(
|
||||||
|
@SerializedName("ban_info")
|
||||||
|
val banInfo: BanInfo
|
||||||
|
) : ApiError() {
|
||||||
|
|
||||||
|
data class BanInfo(
|
||||||
|
@SerializedName("member_name")
|
||||||
|
val memberName: String,
|
||||||
|
val message: String,
|
||||||
|
@SerializedName("access_token")
|
||||||
|
val accessToken: String,
|
||||||
|
@SerializedName("restore_url")
|
||||||
|
val restoreUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.meloda.fast.api.network
|
|||||||
import com.meloda.fast.api.UserConfig
|
import com.meloda.fast.api.UserConfig
|
||||||
import com.meloda.fast.api.VKConstants
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.api.network.account.AccountUrls
|
import com.meloda.fast.api.network.account.AccountUrls
|
||||||
|
import com.meloda.fast.api.network.ota.OtaUrls
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
@@ -14,7 +15,7 @@ class AuthInterceptor : Interceptor {
|
|||||||
|
|
||||||
val url = builder.build().toUrl().toString()
|
val url = builder.build().toUrl().toString()
|
||||||
|
|
||||||
if (!url.contains("upload.php")) {
|
if (!url.contains("upload.php") && !url.contains(OtaUrls.GetActualUrl)) {
|
||||||
builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
|
builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import java.lang.reflect.Type
|
|||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
class ResultCallFactory : CallAdapter.Factory() {
|
class ResultCallFactory(private val gson: Gson) : CallAdapter.Factory() {
|
||||||
override fun get(
|
override fun get(
|
||||||
returnType: Type,
|
returnType: Type,
|
||||||
annotations: Array<out Annotation>,
|
annotations: Array<out Annotation>,
|
||||||
retrofit: Retrofit
|
retrofit: Retrofit,
|
||||||
): CallAdapter<*, *>? {
|
): CallAdapter<*, *>? {
|
||||||
val rawReturnType: Class<*> = getRawType(returnType)
|
val rawReturnType: Class<*> = getRawType(returnType)
|
||||||
if (rawReturnType == Call::class.java) {
|
if (rawReturnType == Call::class.java) {
|
||||||
@@ -27,9 +27,9 @@ class ResultCallFactory : CallAdapter.Factory() {
|
|||||||
if (getRawType(callInnerType) == ApiAnswer::class.java) {
|
if (getRawType(callInnerType) == ApiAnswer::class.java) {
|
||||||
if (callInnerType is ParameterizedType) {
|
if (callInnerType is ParameterizedType) {
|
||||||
val resultInnerType = getParameterUpperBound(0, callInnerType)
|
val resultInnerType = getParameterUpperBound(0, callInnerType)
|
||||||
return ResultCallAdapter<Any?>(resultInnerType)
|
return ResultCallAdapter<Any?>(resultInnerType, gson)
|
||||||
}
|
}
|
||||||
return ResultCallAdapter<Nothing>(Nothing::class.java)
|
return ResultCallAdapter<Nothing>(Nothing::class.java, gson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,30 +58,29 @@ internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : C
|
|||||||
abstract fun cloneImpl(): Call<Out>
|
abstract fun cloneImpl(): Call<Out>
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ResultCallAdapter<R>(private val type: Type) : CallAdapter<R, Call<ApiAnswer<R>>> {
|
private class ResultCallAdapter<R>(private val type: Type, private val gson: Gson) : CallAdapter<R, Call<ApiAnswer<R>>> {
|
||||||
|
|
||||||
override fun responseType() = type
|
override fun responseType() = type
|
||||||
|
|
||||||
override fun adapt(call: Call<R>): Call<ApiAnswer<R>> = ResultCall(call)
|
override fun adapt(call: Call<R>): Call<ApiAnswer<R>> = ResultCall(call, gson)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, ApiAnswer<T>>(proxy) {
|
internal class ResultCall<T>(proxy: Call<T>, private val gson: Gson) : CallDelegate<T, ApiAnswer<T>>(proxy) {
|
||||||
|
|
||||||
override fun enqueueImpl(callback: Callback<ApiAnswer<T>>) {
|
override fun enqueueImpl(callback: Callback<ApiAnswer<T>>) {
|
||||||
proxy.enqueue(ResultCallback(this, callback))
|
proxy.enqueue(ResultCallback(this, callback, gson))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cloneImpl(): ResultCall<T> {
|
override fun cloneImpl(): ResultCall<T> {
|
||||||
return ResultCall(proxy.clone())
|
return ResultCall(proxy.clone(), gson)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ResultCallback<T>(
|
private class ResultCallback<T>(
|
||||||
private val proxy: ResultCall<T>,
|
private val proxy: ResultCall<T>,
|
||||||
private val callback: Callback<ApiAnswer<T>>
|
private val callback: Callback<ApiAnswer<T>>,
|
||||||
|
private val gson: Gson
|
||||||
) : Callback<T> {
|
) : Callback<T> {
|
||||||
|
|
||||||
val gson = Gson()
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||||
val result: ApiAnswer<T> =
|
val result: ApiAnswer<T> =
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
@@ -117,13 +116,11 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, ApiAnswer<T>>(pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkErrors(call: Call<T>, result: ApiAnswer<*>): Boolean {
|
private fun checkErrors(call: Call<T>, result: ApiAnswer<*>): Boolean {
|
||||||
if (!result.isSuccessful()) {
|
if (result.isError()) {
|
||||||
result.error.throwable?.run {
|
result.error.throwable?.run {
|
||||||
onFailure(call, this)
|
onFailure(call, this)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -143,8 +140,16 @@ sealed class ApiAnswer<out R> {
|
|||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
fun isSuccessful(): Boolean {
|
fun isSuccessful(): Boolean {
|
||||||
contract {
|
contract {
|
||||||
returns(false) implies (this@ApiAnswer is Error)
|
returns(true) implies (this@ApiAnswer is Success)
|
||||||
}
|
}
|
||||||
return this is Success
|
return this is Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
fun isError(): Boolean {
|
||||||
|
contract {
|
||||||
|
returns(true) implies (this@ApiAnswer is Error)
|
||||||
|
}
|
||||||
|
return this is Error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,37 +4,6 @@ object VkUrls {
|
|||||||
|
|
||||||
const val OAUTH = "https://oauth.vk.com"
|
const val OAUTH = "https://oauth.vk.com"
|
||||||
const val API = "https://api.vk.com/method"
|
const val API = "https://api.vk.com/method"
|
||||||
|
|
||||||
object Auth {
|
|
||||||
const val DirectAuth = "$OAUTH/token"
|
|
||||||
const val SendSms = "$API/auth.validatePhone"
|
|
||||||
}
|
|
||||||
|
|
||||||
object Conversations {
|
|
||||||
const val Get = "$API/messages.getConversations"
|
|
||||||
const val Delete = "$API/messages.deleteConversation"
|
|
||||||
const val Pin = "$API/messages.pinConversation"
|
|
||||||
const val Unpin = "$API/messages.unpinConversation"
|
|
||||||
const val ReorderPinned = "$API/messages.reorderPinnedConversations"
|
|
||||||
}
|
|
||||||
|
|
||||||
object Users {
|
|
||||||
const val GetById = "$API/users.get"
|
|
||||||
}
|
|
||||||
|
|
||||||
object Messages {
|
|
||||||
const val GetHistory = "$API/messages.getHistory"
|
|
||||||
const val Send = "$API/messages.send"
|
|
||||||
const val MarkAsImportant = "$API/messages.markAsImportant"
|
|
||||||
const val GetLongPollServer = "$API/messages.getLongPollServer"
|
|
||||||
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
|
|
||||||
const val Pin = "$API/messages.pin"
|
|
||||||
const val Unpin = "$API/messages.unpin"
|
|
||||||
const val Delete = "$API/messages.delete"
|
|
||||||
const val Edit = "$API/messages.edit"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AuthDirectResponse(
|
data class AuthDirectResponse(
|
||||||
@SerializedName("access_token") val accessToken: String? = null,
|
@SerializedName("access_token") val accessToken: String?,
|
||||||
@SerializedName("user_id") val userId: Int? = null,
|
@SerializedName("user_id") val userId: Int?,
|
||||||
@SerializedName("trusted_hash") val twoFaHash: String? = null,
|
@SerializedName("trusted_hash") val twoFaHash: String?,
|
||||||
@SerializedName("validation_sid") val validationSid: String? = null
|
@SerializedName("validation_sid") val validationSid: String?,
|
||||||
|
@SerializedName("validation_type") val validationType: String?,
|
||||||
|
@SerializedName("phone_mask") val phoneMask: String?,
|
||||||
|
@SerializedName("redirect_uri") val redirectUrl: String?,
|
||||||
|
@SerializedName("validation_resend") val validationResend: String?,
|
||||||
|
@SerializedName("cant_get_code_open_restore") val isCanNotGetCodeNeedToOpenRestore: Boolean
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ package com.meloda.fast.api.network.conversations
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.meloda.fast.api.model.base.BaseVkConversation
|
import com.meloda.fast.api.model.data.BaseVkConversation
|
||||||
import com.meloda.fast.api.model.base.BaseVkGroup
|
import com.meloda.fast.api.model.base.BaseVkGroup
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||||
import com.meloda.fast.api.model.base.BaseVkUser
|
import com.meloda.fast.api.model.base.BaseVkUser
|
||||||
|
|||||||
@@ -189,5 +189,51 @@ data class MessagesGetByIdRequest(
|
|||||||
extended?.let { this["extended"] = it.intString }
|
extended?.let { this["extended"] = it.intString }
|
||||||
fields?.let { this["fields"] = it }
|
fields?.let { this["fields"] = it }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesGetChatRequest(
|
||||||
|
val chatId: Int,
|
||||||
|
val fields: String? = null
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"chat_id" to chatId.toString()
|
||||||
|
).apply {
|
||||||
|
fields?.let { this["fields"] = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesGetConversationMembersRequest(
|
||||||
|
val peerId: Int,
|
||||||
|
val offset: Int? = null,
|
||||||
|
val count: Int? = null,
|
||||||
|
val extended: Boolean? = null,
|
||||||
|
val fields: String? = null
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"peer_id" to peerId.toString()
|
||||||
|
).apply {
|
||||||
|
offset?.let { this["offset"] = it.toString() }
|
||||||
|
count?.let { this["count"] = it.toString() }
|
||||||
|
extended?.let { this["extended"] = it.toString() }
|
||||||
|
fields?.let { this["fields"] = it }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesRemoveChatUserRequest(
|
||||||
|
val chatId: Int,
|
||||||
|
val memberId: Int
|
||||||
|
) : Parcelable {
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"chat_id" to chatId.toString(),
|
||||||
|
"member_id" to memberId.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.meloda.fast.api.network.messages
|
package com.meloda.fast.api.network.messages
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.meloda.fast.api.model.base.BaseVkConversation
|
import com.meloda.fast.api.model.base.*
|
||||||
import com.meloda.fast.api.model.base.BaseVkGroup
|
import com.meloda.fast.api.model.data.BaseVkConversation
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
|
||||||
import com.meloda.fast.api.model.base.BaseVkUser
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -23,3 +21,11 @@ data class MessagesGetByIdResponse(
|
|||||||
val profiles: List<BaseVkUser>?,
|
val profiles: List<BaseVkUser>?,
|
||||||
val groups: List<BaseVkGroup>?
|
val groups: List<BaseVkGroup>?
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesGetConversationMembersResponse(
|
||||||
|
val count: Int,
|
||||||
|
val items: List<BaseVkChatMember> = emptyList(),
|
||||||
|
val profiles: List<BaseVkUser>?,
|
||||||
|
val groups: List<BaseVkGroup>?
|
||||||
|
) : Parcelable
|
||||||
|
|||||||
@@ -15,5 +15,8 @@ object MessagesUrls {
|
|||||||
const val Edit = "${VkUrls.API}/messages.edit"
|
const val Edit = "${VkUrls.API}/messages.edit"
|
||||||
const val GetById = "${VkUrls.API}/messages.getById"
|
const val GetById = "${VkUrls.API}/messages.getById"
|
||||||
const val MarkAsRead = "${VkUrls.API}/messages.markAsRead"
|
const val MarkAsRead = "${VkUrls.API}/messages.markAsRead"
|
||||||
|
const val GetChat = "${VkUrls.API}/messages.getChat"
|
||||||
|
const val GetConversationMembers = "${VkUrls.API}/messages.getConversationMembers"
|
||||||
|
const val RemoveChatUser = "${VkUrls.API}/messages.removeChatUser"
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,5 +8,4 @@ abstract class BaseActivity : AppCompatActivity {
|
|||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
|
||||||
constructor(@LayoutRes resId: Int) : super(resId)
|
constructor(@LayoutRes resId: Int) : super(resId)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,47 +1,11 @@
|
|||||||
package com.meloda.fast.base
|
package com.meloda.fast.base
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.meloda.fast.screens.main.MainActivity
|
|
||||||
|
|
||||||
abstract class BaseFragment : Fragment {
|
abstract class BaseFragment : Fragment {
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
|
||||||
constructor(@LayoutRes resId: Int) : super(resId)
|
constructor(@LayoutRes resId: Int) : super(resId)
|
||||||
|
|
||||||
protected var shouldNavBarShown: Boolean = true
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
if (arguments == null) arguments = Bundle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
(requireActivity() as? MainActivity)?.run {
|
|
||||||
toggleNavBarVisibility(shouldNavBarShown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val activityRouter
|
|
||||||
get() = run {
|
|
||||||
if (requireActivity() is MainActivity) {
|
|
||||||
(requireActivity() as MainActivity).router
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requireActivityRouter() = requireNotNull(activityRouter)
|
|
||||||
|
|
||||||
fun startProgress() = toggleProgress(true)
|
|
||||||
fun stopProgress() = toggleProgress(false)
|
|
||||||
|
|
||||||
protected open fun toggleProgress(isProgressing: Boolean) {}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.meloda.fast.base.adapter
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import com.meloda.fast.model.base.AdapterDiffItem
|
||||||
|
|
||||||
|
class AsyncDiffItemAdapter(
|
||||||
|
customDiffCallback: DiffUtil.ItemCallback<AdapterDiffItem>? = null,
|
||||||
|
vararg delegates: AdapterDelegate<out List<AdapterDiffItem>>,
|
||||||
|
) : AsyncListDifferDelegationAdapter<AdapterDiffItem>(customDiffCallback ?: DIFF_CALLBACK) {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
vararg delegates: AdapterDelegate<out List<AdapterDiffItem>>,
|
||||||
|
) : this(customDiffCallback = null) {
|
||||||
|
delegates.forEach(::addDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegates.forEach(::addDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addDelegates(vararg delegates: AdapterDelegate<out List<AdapterDiffItem>>) {
|
||||||
|
delegates.forEach(::addDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun addDelegate(delegate: AdapterDelegate<out List<AdapterDiffItem>>) {
|
||||||
|
(delegate as? AdapterDelegate<List<AdapterDiffItem>>)?.let(delegatesManager::addDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty() = itemCount == 0
|
||||||
|
fun isNotEmpty() = itemCount > 0
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<AdapterDiffItem>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: AdapterDiffItem,
|
||||||
|
newItem: AdapterDiffItem,
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.areItemsTheSame(newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: AdapterDiffItem,
|
||||||
|
newItem: AdapterDiffItem,
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.areContentsTheSame(newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,11 @@ import android.widget.Filter
|
|||||||
import android.widget.Filterable
|
import android.widget.Filterable
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import com.meloda.fast.model.DataItem
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST")
|
@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST")
|
||||||
abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
abstract class BaseAdapter<T : Any, VH : BaseHolder> constructor(
|
||||||
var context: Context,
|
var context: Context,
|
||||||
diffUtil: DiffUtil.ItemCallback<T>,
|
diffUtil: DiffUtil.ItemCallback<T>,
|
||||||
preAddedValues: List<T> = emptyList(),
|
preAddedValues: List<T> = emptyList(),
|
||||||
@@ -59,27 +58,18 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
|||||||
fun add(
|
fun add(
|
||||||
item: T,
|
item: T,
|
||||||
position: Int? = null,
|
position: Int? = null,
|
||||||
beforeFooter: Boolean = false,
|
|
||||||
commitCallback: (() -> Unit)? = null
|
commitCallback: (() -> Unit)? = null
|
||||||
) = addAll(listOf(item), position, beforeFooter, commitCallback)
|
) = addAll(listOf(item), position, commitCallback)
|
||||||
|
|
||||||
fun addAll(
|
fun addAll(
|
||||||
items: List<T>,
|
items: List<T>,
|
||||||
position: Int? = null,
|
position: Int? = null,
|
||||||
beforeFooter: Boolean = false,
|
|
||||||
commitCallback: (() -> Unit)? = null
|
commitCallback: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
adapterScope.launch {
|
adapterScope.launch {
|
||||||
val newList = cloneCurrentList()
|
val newList = cloneCurrentList()
|
||||||
if (position == null) {
|
if (position == null) {
|
||||||
val mutableItems = items.toMutableList()
|
val mutableItems = items.toMutableList()
|
||||||
if (beforeFooter && newList.lastOrNull() is DataItem.Footer) {
|
|
||||||
newList.removeLastOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beforeFooter) {
|
|
||||||
mutableItems += DataItem.Footer as T
|
|
||||||
}
|
|
||||||
|
|
||||||
newList.addAll(mutableItems)
|
newList.addAll(mutableItems)
|
||||||
cleanList.addAll(mutableItems)
|
cleanList.addAll(mutableItems)
|
||||||
@@ -100,40 +90,34 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
|||||||
fun removeAll(items: List<T>, commitCallback: (() -> Unit)? = null) {
|
fun removeAll(items: List<T>, commitCallback: (() -> Unit)? = null) {
|
||||||
val newList = cloneCurrentList()
|
val newList = cloneCurrentList()
|
||||||
newList.removeAll(items)
|
newList.removeAll(items)
|
||||||
submitList(newList, commitCallback)
|
|
||||||
|
|
||||||
cleanList.removeAll(items)
|
cleanList.removeAll(items)
|
||||||
|
|
||||||
|
submitList(newList, commitCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) {
|
fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) {
|
||||||
val newList = cloneCurrentList()
|
val newList = cloneCurrentList()
|
||||||
newList.removeAt(index)
|
newList.removeAt(index)
|
||||||
submitList(newList, commitCallback)
|
|
||||||
|
|
||||||
cleanList.removeAt(index)
|
cleanList.removeAt(index)
|
||||||
|
|
||||||
|
submitList(newList, commitCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback)
|
fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback)
|
||||||
|
|
||||||
fun setItem(
|
fun setItem(
|
||||||
item: T,
|
item: T,
|
||||||
withHeader: Boolean = false,
|
|
||||||
withFooter: Boolean = false,
|
|
||||||
commitCallback: (() -> Unit)? = null
|
commitCallback: (() -> Unit)? = null
|
||||||
) = setItems(listOf(item), withHeader, withFooter, commitCallback)
|
) = setItems(listOf(item), commitCallback)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun setItems(
|
fun setItems(
|
||||||
list: List<T>?,
|
list: List<T>?,
|
||||||
withHeader: Boolean = false,
|
|
||||||
withFooter: Boolean = false,
|
|
||||||
commitCallback: (() -> Unit)? = null
|
commitCallback: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
adapterScope.launch {
|
adapterScope.launch {
|
||||||
val items = mutableListOf<T>()
|
val items = mutableListOf<T>()
|
||||||
if (withHeader) items.add(DataItem.Header as T)
|
|
||||||
if (!list.isNullOrEmpty()) items.addAll(list)
|
if (!list.isNullOrEmpty()) items.addAll(list)
|
||||||
if (withFooter) items.add(DataItem.Footer as T)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (items == currentList) {
|
if (items == currentList) {
|
||||||
@@ -165,9 +149,9 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
|||||||
fun setItem(position: Int, item: T, commitCallback: (() -> Unit)? = null) {
|
fun setItem(position: Int, item: T, commitCallback: (() -> Unit)? = null) {
|
||||||
val newList = cloneCurrentList()
|
val newList = cloneCurrentList()
|
||||||
newList[position] = item
|
newList[position] = item
|
||||||
submitList(newList, commitCallback)
|
|
||||||
|
|
||||||
cleanList[position] = item
|
cleanList[position] = item
|
||||||
|
|
||||||
|
submitList(newList, commitCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isEmpty() = currentList.isEmpty()
|
fun isEmpty() = currentList.isEmpty()
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.meloda.fast.base.adapter
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.meloda.fast.extensions.dpToPx
|
|
||||||
import com.meloda.fast.util.AndroidUtils
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class EmptyHeaderAdapter(
|
|
||||||
var context: Context
|
|
||||||
) : RecyclerView.Adapter<EmptyHeaderAdapter.Holder>() {
|
|
||||||
|
|
||||||
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = Holder(generateHeaderView())
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = 1
|
|
||||||
|
|
||||||
private fun generateHeaderView() = View(context).apply {
|
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
56.dpToPx()
|
|
||||||
)
|
|
||||||
isClickable = false
|
|
||||||
isEnabled = false
|
|
||||||
isFocusable = false
|
|
||||||
isInvisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.meloda.fast.base.adapter
|
package com.meloda.fast.base.adapter
|
||||||
|
|
||||||
interface OnItemClickListener {
|
fun interface OnItemClickListener<T> {
|
||||||
fun onItemClick(position: Int)
|
fun onItemClick(item: T)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnItemLongClickListener {
|
fun interface OnItemLongClickListener<T> {
|
||||||
fun onItemLongClick(position: Int)
|
fun onLongItemClick(item: T): Boolean
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.meloda.fast.base.screen
|
||||||
|
|
||||||
|
import com.github.terrakok.cicerone.Router
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
|
interface AppScreen<ArgType, ResultType> {
|
||||||
|
val resultFlow: MutableSharedFlow<ResultType>
|
||||||
|
|
||||||
|
var args: ArgType
|
||||||
|
|
||||||
|
fun show(router: Router, args: ArgType)
|
||||||
|
|
||||||
|
fun getArguments(): ArgType = args
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun <ArgType, ResultType> AppScreen<ArgType, ResultType>.createResultFlow(): MutableSharedFlow<ResultType> {
|
||||||
|
return MutableSharedFlow(
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,110 +1,97 @@
|
|||||||
package com.meloda.fast.base.viewmodel
|
package com.meloda.fast.base.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.meloda.fast.api.base.ApiError
|
import com.meloda.fast.api.base.ApiError
|
||||||
import com.meloda.fast.api.network.ApiAnswer
|
import com.meloda.fast.api.network.*
|
||||||
import com.meloda.fast.api.network.AuthorizationError
|
import com.meloda.fast.ext.isTrue
|
||||||
import com.meloda.fast.api.network.CaptchaRequiredError
|
import com.meloda.fast.ext.notNull
|
||||||
import com.meloda.fast.api.network.ValidationRequiredError
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
|
||||||
abstract class BaseViewModel : ViewModel() {
|
abstract class BaseViewModel : ViewModel() {
|
||||||
|
|
||||||
var unknownErrorDefaultText: String = ""
|
open suspend fun sendSingleEvent(event: VkEvent) {}
|
||||||
|
|
||||||
protected val tasksEventChannel = Channel<VkEvent>()
|
suspend fun <T> sendRequestNotNull(
|
||||||
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
onError: ErrorHandler? = null,
|
||||||
|
request: suspend () -> ApiAnswer<T>
|
||||||
|
): T = sendRequest(onError, request).notNull()
|
||||||
|
|
||||||
protected val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
suspend fun <T> sendRequest(
|
||||||
viewModelScope.launch { onException(throwable) }
|
onError: ErrorHandler? = null,
|
||||||
}
|
request: suspend () -> ApiAnswer<T>,
|
||||||
|
): T? {
|
||||||
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
|
return when (val response = request()) {
|
||||||
return viewModelScope.launch(exceptionHandler, block = block)
|
is ApiAnswer.Success -> response.data
|
||||||
}
|
|
||||||
|
|
||||||
protected suspend fun <T> makeSuspendJob(
|
|
||||||
job: suspend () -> ApiAnswer<T>, onAnswer: suspend (T) -> Unit = {},
|
|
||||||
onStart: (suspend () -> Unit)? = null,
|
|
||||||
onEnd: (suspend () -> Unit)? = null,
|
|
||||||
onError: (suspend (Throwable) -> Unit)? = null
|
|
||||||
): ApiAnswer<T> {
|
|
||||||
onStart?.invoke() ?: onStart()
|
|
||||||
val response = job()
|
|
||||||
|
|
||||||
when (response) {
|
|
||||||
is ApiAnswer.Success -> onAnswer(response.data)
|
|
||||||
is ApiAnswer.Error -> {
|
is ApiAnswer.Error -> {
|
||||||
onError?.invoke(response.error) ?: checkErrors(response.error)
|
val error = response.error
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnd?.invoke()
|
if (!onError?.handleError(error).isTrue) {
|
||||||
|
checkErrors(error)
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
null
|
||||||
}
|
|
||||||
|
|
||||||
protected fun <T> makeJob(
|
|
||||||
job: suspend () -> ApiAnswer<T>,
|
|
||||||
onAnswer: suspend (T) -> Unit = {},
|
|
||||||
onStart: (suspend () -> Unit)? = null,
|
|
||||||
onEnd: (suspend () -> Unit)? = null,
|
|
||||||
onError: (suspend (Throwable) -> Unit)? = null
|
|
||||||
): Job = viewModelScope.launch {
|
|
||||||
onStart?.invoke() ?: onStart()
|
|
||||||
when (val response = job()) {
|
|
||||||
is ApiAnswer.Success -> onAnswer(response.data)
|
|
||||||
is ApiAnswer.Error -> {
|
|
||||||
onError?.invoke(response.error) ?: checkErrors(response.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
it.invokeOnCompletion {
|
|
||||||
viewModelScope.launch {
|
|
||||||
onEnd?.invoke() ?: onStop()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open suspend fun onException(throwable: Throwable) {
|
|
||||||
checkErrors(throwable)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected suspend fun onStart() {
|
|
||||||
sendEvent(StartProgressEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected suspend fun onStop() {
|
|
||||||
sendEvent(StopProgressEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected suspend fun <T : VkEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
|
||||||
|
|
||||||
protected suspend fun checkErrors(throwable: Throwable) {
|
protected suspend fun checkErrors(throwable: Throwable) {
|
||||||
when (throwable) {
|
when (throwable) {
|
||||||
|
is TokenExpiredError -> {
|
||||||
|
sendSingleEvent(TokenExpiredErrorEvent)
|
||||||
|
}
|
||||||
is AuthorizationError -> {
|
is AuthorizationError -> {
|
||||||
sendEvent(AuthorizationErrorEvent)
|
sendSingleEvent(AuthorizationErrorEvent)
|
||||||
|
}
|
||||||
|
is UserBannedError -> {
|
||||||
|
throwable.banInfo.let { banInfo ->
|
||||||
|
sendSingleEvent(
|
||||||
|
UserBannedEvent(
|
||||||
|
memberName = banInfo.memberName,
|
||||||
|
message = banInfo.message,
|
||||||
|
restoreUrl = banInfo.restoreUrl,
|
||||||
|
accessToken = banInfo.accessToken
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is ValidationRequiredError -> {
|
is ValidationRequiredError -> {
|
||||||
sendEvent(ValidationRequiredEvent(throwable.validationSid))
|
sendSingleEvent(
|
||||||
|
ValidationRequiredEvent(
|
||||||
|
sid = throwable.validationSid,
|
||||||
|
redirectUri = throwable.redirectUri,
|
||||||
|
phoneMask = throwable.phoneMask,
|
||||||
|
validationType = throwable.validationType,
|
||||||
|
canResendSms = throwable.validationResend == "sms",
|
||||||
|
codeError = null
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is CaptchaRequiredError -> {
|
is CaptchaRequiredError -> {
|
||||||
sendEvent(CaptchaRequiredEvent(throwable.captchaSid, throwable.captchaImg))
|
sendSingleEvent(
|
||||||
|
CaptchaRequiredEvent(
|
||||||
|
sid = throwable.captchaSid,
|
||||||
|
image = throwable.captchaImg
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is ApiError -> {
|
is ApiError -> {
|
||||||
sendEvent(ErrorTextEvent(errorText = throwable.errorMessage ?: unknownErrorDefaultText))
|
sendSingleEvent(
|
||||||
|
if (throwable.errorMessage == null) {
|
||||||
|
UnknownErrorEvent
|
||||||
|
} else {
|
||||||
|
ErrorTextEvent(errorText = requireNotNull(throwable.errorMessage))
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
sendEvent(ErrorTextEvent(throwable.message ?: unknownErrorDefaultText))
|
sendSingleEvent(
|
||||||
|
if (throwable.message == null) {
|
||||||
|
UnknownErrorEvent
|
||||||
|
} else {
|
||||||
|
ErrorTextEvent(requireNotNull(throwable.message))
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,10 @@ import android.view.View
|
|||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.meloda.fast.base.BaseFragment
|
import com.meloda.fast.base.BaseFragment
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
|
@Deprecated("", ReplaceWith("BaseFragment"))
|
||||||
|
abstract class BaseViewModelFragment<VM : DeprecatedBaseViewModel> : BaseFragment {
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
|
|||||||
ViewModelUtils.parseEvent(this, event)
|
ViewModelUtils.parseEvent(this, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun <T : BaseViewModel> subscribeToViewModel(viewModel: T) {
|
protected fun <T : DeprecatedBaseViewModel> subscribeToViewModel(viewModel: T) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.tasksEvent.collect { onEvent(it) }
|
viewModel.tasksEvent.collect { onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.meloda.fast.base.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.meloda.fast.api.base.ApiError
|
||||||
|
import com.meloda.fast.api.network.*
|
||||||
|
import com.meloda.fast.ext.isTrue
|
||||||
|
import com.meloda.fast.ext.notNull
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Deprecated("rewrite")
|
||||||
|
abstract class DeprecatedBaseViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val tasksEventChannel = Channel<VkEvent>()
|
||||||
|
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
||||||
|
|
||||||
|
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
viewModelScope.launch { onException(throwable) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
|
||||||
|
return viewModelScope.launch(exceptionHandler, block = block)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> sendRequestNotNull(
|
||||||
|
onError: ErrorHandler? = null,
|
||||||
|
request: suspend () -> ApiAnswer<T>
|
||||||
|
): T = sendRequest(onError, request).notNull()
|
||||||
|
|
||||||
|
suspend fun <T> sendRequest(
|
||||||
|
onError: ErrorHandler? = null,
|
||||||
|
request: suspend () -> ApiAnswer<T>,
|
||||||
|
): T? {
|
||||||
|
return when (val response = request()) {
|
||||||
|
is ApiAnswer.Success -> response.data
|
||||||
|
is ApiAnswer.Error -> {
|
||||||
|
val error = response.error
|
||||||
|
|
||||||
|
if (!onError?.handleError(error).isTrue) {
|
||||||
|
checkErrors(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 05.04.2023, Danil Nikolaev: переписать makeJob на sendRequest (oh boy, писать дохуя)
|
||||||
|
// TODO: 05.04.2023, Danil Nikolaev: переписать Conversations Screen на новую архитектуру, пока что оставить View
|
||||||
|
|
||||||
|
protected fun <T> makeJob(
|
||||||
|
job: suspend () -> ApiAnswer<T>,
|
||||||
|
onAnswer: suspend (T) -> Unit = {},
|
||||||
|
onStart: (suspend () -> Unit)? = null,
|
||||||
|
onEnd: (suspend () -> Unit)? = null,
|
||||||
|
onError: (suspend (Throwable) -> Unit)? = null,
|
||||||
|
onAnyResult: (suspend () -> Unit)? = null,
|
||||||
|
): Job = viewModelScope.launch {
|
||||||
|
onStart?.invoke()
|
||||||
|
when (val response = job()) {
|
||||||
|
is ApiAnswer.Success -> {
|
||||||
|
onAnswer(response.data)
|
||||||
|
onAnyResult?.invoke()
|
||||||
|
}
|
||||||
|
is ApiAnswer.Error -> {
|
||||||
|
onError?.invoke(response.error) ?: checkErrors(response.error)
|
||||||
|
onAnyResult?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
it.invokeOnCompletion {
|
||||||
|
viewModelScope.launch {
|
||||||
|
onEnd?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open suspend fun onException(throwable: Throwable) {
|
||||||
|
checkErrors(throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend fun <T : VkEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
||||||
|
|
||||||
|
protected suspend fun checkErrors(throwable: Throwable) {
|
||||||
|
when (throwable) {
|
||||||
|
is TokenExpiredError -> sendEvent(TokenExpiredErrorEvent)
|
||||||
|
is AuthorizationError -> sendEvent(AuthorizationErrorEvent)
|
||||||
|
is UserBannedError -> {
|
||||||
|
val banInfo = throwable.banInfo
|
||||||
|
sendEvent(
|
||||||
|
UserBannedEvent(
|
||||||
|
memberName = banInfo.memberName,
|
||||||
|
message = banInfo.message,
|
||||||
|
restoreUrl = banInfo.restoreUrl,
|
||||||
|
accessToken = banInfo.accessToken
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ValidationRequiredError -> {
|
||||||
|
sendEvent(
|
||||||
|
ValidationRequiredEvent(
|
||||||
|
sid = throwable.validationSid,
|
||||||
|
redirectUri = throwable.redirectUri,
|
||||||
|
phoneMask = throwable.phoneMask,
|
||||||
|
validationType = throwable.validationType,
|
||||||
|
canResendSms = throwable.validationResend == "sms",
|
||||||
|
codeError = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CaptchaRequiredError -> sendEvent(
|
||||||
|
CaptchaRequiredEvent(
|
||||||
|
sid = throwable.captchaSid,
|
||||||
|
image = throwable.captchaImg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is ApiError -> sendEvent(
|
||||||
|
if (throwable.errorMessage == null) {
|
||||||
|
UnknownErrorEvent
|
||||||
|
} else {
|
||||||
|
ErrorTextEvent(errorText = requireNotNull(throwable.errorMessage))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else -> sendEvent(
|
||||||
|
if (throwable.message == null) {
|
||||||
|
UnknownErrorEvent
|
||||||
|
} else {
|
||||||
|
ErrorTextEvent(requireNotNull(throwable.message))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.meloda.fast.base.viewmodel
|
||||||
|
|
||||||
|
fun interface ErrorHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if error has been handled manually
|
||||||
|
*/
|
||||||
|
suspend fun handleError(error: Throwable): Boolean
|
||||||
|
}
|
||||||
@@ -1,18 +1,30 @@
|
|||||||
package com.meloda.fast.base.viewmodel
|
package com.meloda.fast.base.viewmodel
|
||||||
|
|
||||||
abstract class VkEvent
|
import com.meloda.fast.model.base.UiText
|
||||||
abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent()
|
|
||||||
abstract class VkProgressEvent : VkEvent()
|
|
||||||
|
|
||||||
|
abstract class VkEvent
|
||||||
|
|
||||||
|
abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent()
|
||||||
|
|
||||||
|
object UnknownErrorEvent : VkErrorEvent()
|
||||||
open class ErrorTextEvent(override val errorText: String) : VkErrorEvent()
|
open class ErrorTextEvent(override val errorText: String) : VkErrorEvent()
|
||||||
|
|
||||||
object AuthorizationErrorEvent : VkErrorEvent()
|
object AuthorizationErrorEvent : VkErrorEvent()
|
||||||
|
object TokenExpiredErrorEvent : VkErrorEvent()
|
||||||
data class CaptchaRequiredEvent(val sid: String, val image: String) : VkErrorEvent()
|
data class CaptchaRequiredEvent(val sid: String, val image: String) : VkErrorEvent()
|
||||||
data class ValidationRequiredEvent(val sid: String) : VkErrorEvent()
|
data class ValidationRequiredEvent(
|
||||||
|
val sid: String,
|
||||||
|
val redirectUri: String,
|
||||||
|
val phoneMask: String,
|
||||||
|
val validationType: String,
|
||||||
|
val canResendSms: Boolean,
|
||||||
|
val codeError: UiText?
|
||||||
|
) : VkErrorEvent()
|
||||||
|
|
||||||
object StartProgressEvent : VkProgressEvent()
|
data class UserBannedEvent(
|
||||||
object StopProgressEvent : VkProgressEvent()
|
val memberName: String, val message: String, val restoreUrl: String, val accessToken: String,
|
||||||
|
) : VkErrorEvent()
|
||||||
|
|
||||||
interface VkEventCallback<in T : Any> {
|
fun interface VkEventCallback<in T : Any> {
|
||||||
fun onEvent(event: T)
|
fun onEvent(event: T)
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,13 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
import com.meloda.fast.api.UserConfig
|
import com.meloda.fast.api.UserConfig
|
||||||
import com.meloda.fast.base.BaseFragment
|
import com.meloda.fast.ext.showDialog
|
||||||
import com.meloda.fast.screens.main.MainActivity
|
import com.meloda.fast.model.base.UiText
|
||||||
import com.meloda.fast.util.ViewUtils.showErrorDialog
|
import com.meloda.fast.screens.main.activity.MainActivity
|
||||||
|
|
||||||
object ViewModelUtils {
|
object ViewModelUtils {
|
||||||
|
|
||||||
|
@Deprecated("rewrite")
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
fun parseEvent(activity: FragmentActivity, event: VkEvent) {
|
fun parseEvent(activity: FragmentActivity, event: VkEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -24,26 +25,47 @@ object ViewModelUtils {
|
|||||||
activity.finishAffinity()
|
activity.finishAffinity()
|
||||||
activity.startActivity(Intent(activity, MainActivity::class.java))
|
activity.startActivity(Intent(activity, MainActivity::class.java))
|
||||||
}
|
}
|
||||||
|
is TokenExpiredErrorEvent -> {
|
||||||
|
Toast.makeText(
|
||||||
|
activity, R.string.token_expired, Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
UserConfig.clear()
|
||||||
|
activity.finishAffinity()
|
||||||
|
activity.startActivity(Intent(activity, MainActivity::class.java))
|
||||||
|
}
|
||||||
|
is UserBannedEvent -> {
|
||||||
|
// TODO: 17.04.2023, Danil Nikolaev: handle banned event
|
||||||
|
// (activity as? MainActivity)?.accessRouter()?.newRootScreen(
|
||||||
|
// Screens.UserBanned(
|
||||||
|
// memberName = event.memberName,
|
||||||
|
// message = event.message,
|
||||||
|
// restoreUrl = event.restoreUrl,
|
||||||
|
// accessToken = event.accessToken
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
is UnknownErrorEvent -> {
|
||||||
|
activity.showDialog(
|
||||||
|
title = UiText.Resource(R.string.title_error),
|
||||||
|
message = UiText.Resource(R.string.unknown_error_occurred),
|
||||||
|
positiveText = UiText.Resource(R.string.ok)
|
||||||
|
)
|
||||||
|
}
|
||||||
is VkErrorEvent -> {
|
is VkErrorEvent -> {
|
||||||
event.errorText?.run {
|
event.errorText?.run {
|
||||||
activity.showErrorDialog(this)
|
activity.showDialog(
|
||||||
|
title = UiText.Resource(R.string.title_error),
|
||||||
|
message = UiText.Simple(this),
|
||||||
|
positiveText = UiText.Resource(R.string.ok)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("rewrite")
|
||||||
fun parseEvent(fragment: Fragment, event: VkEvent) {
|
fun parseEvent(fragment: Fragment, event: VkEvent) {
|
||||||
if (event is VkProgressEvent) {
|
parseEvent(fragment.requireActivity(), event)
|
||||||
if (fragment is BaseFragment) {
|
|
||||||
if (event is StartProgressEvent) {
|
|
||||||
fragment.startProgress()
|
|
||||||
} else if (event is StopProgressEvent) {
|
|
||||||
fragment.stopProgress()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parseEvent(fragment.requireActivity(), event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,94 +1,83 @@
|
|||||||
package com.meloda.fast.common
|
package com.meloda.fast.common
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.ConnectivityManager
|
import android.media.AudioManager
|
||||||
import android.util.Log
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.room.Room
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.meloda.fast.database.AppDatabase
|
import com.meloda.fast.common.di.applicationModule
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import com.meloda.fast.screens.settings.SettingsFragment
|
||||||
|
import com.meloda.fast.util.AndroidUtils
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.android.ext.koin.androidLogger
|
||||||
|
import org.koin.core.context.GlobalContext.startKoin
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
@HiltAndroidApp
|
|
||||||
class AppGlobal : Application() {
|
class AppGlobal : Application() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
lateinit var inputMethodManager: InputMethodManager
|
|
||||||
lateinit var connectivityManager: ConnectivityManager
|
|
||||||
lateinit var clipboardManager: ClipboardManager
|
|
||||||
lateinit var downloadManager: DownloadManager
|
|
||||||
|
|
||||||
lateinit var preferences: SharedPreferences
|
|
||||||
lateinit var resources: Resources
|
|
||||||
lateinit var packageName: String
|
|
||||||
private lateinit var instance: AppGlobal
|
|
||||||
|
|
||||||
lateinit var appDatabase: AppDatabase
|
|
||||||
|
|
||||||
lateinit var packageManager: PackageManager
|
|
||||||
|
|
||||||
var versionName = ""
|
|
||||||
var versionCode = 0
|
|
||||||
|
|
||||||
var screenWidth = 0
|
|
||||||
var screenHeight = 0
|
|
||||||
|
|
||||||
var screenWidth80 = 0
|
|
||||||
|
|
||||||
val Instance get() = instance
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache")
|
if (preferences.getBoolean(
|
||||||
// .fallbackToDestructiveMigration()
|
SettingsFragment.KEY_USE_DYNAMIC_COLORS,
|
||||||
.build()
|
SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS
|
||||||
|
)
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
) {
|
||||||
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
}
|
||||||
|
|
||||||
val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
|
val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
|
||||||
versionName = info.versionName
|
versionName = info.versionName
|
||||||
versionCode = PackageInfoCompat.getLongVersionCode(info).toInt()
|
versionCode = PackageInfoCompat.getLongVersionCode(info).toInt()
|
||||||
|
|
||||||
Companion.resources = resources
|
screenWidth80 = (AndroidUtils.getDisplayWidth() * 0.8).roundToInt()
|
||||||
Companion.packageName = packageName
|
|
||||||
Companion.packageManager = packageManager
|
|
||||||
|
|
||||||
screenWidth = resources.displayMetrics.widthPixels
|
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
screenHeight = resources.displayMetrics.heightPixels
|
|
||||||
|
|
||||||
screenWidth80 = (screenWidth * 0.8).roundToInt()
|
applyDarkTheme()
|
||||||
|
|
||||||
val density = resources.displayMetrics.density
|
initKoin()
|
||||||
val densityDpi = resources.displayMetrics.densityDpi
|
}
|
||||||
val densityScaled = resources.displayMetrics.scaledDensity
|
|
||||||
val xDpi = resources.displayMetrics.xdpi
|
|
||||||
val yDpi = resources.displayMetrics.ydpi
|
|
||||||
|
|
||||||
val diagonal = sqrt(
|
private fun applyDarkTheme() {
|
||||||
(screenWidth * screenWidth - screenHeight * screenHeight).toFloat()
|
val nightMode = preferences.getInt(
|
||||||
|
SettingsFragment.KEY_APPEARANCE_DARK_THEME,
|
||||||
|
SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME
|
||||||
)
|
)
|
||||||
|
AppCompatDelegate.setDefaultNightMode(nightMode)
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(
|
private fun initKoin() {
|
||||||
"Fast::DeviceInfo",
|
startKoin {
|
||||||
"width: $screenWidth; 70% width: $screenWidth80; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
|
androidLogger()
|
||||||
)
|
androidContext(this@AppGlobal)
|
||||||
|
modules(applicationModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
companion object {
|
||||||
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
private lateinit var instance: AppGlobal
|
||||||
clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val preferences: SharedPreferences by lazy {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionName = ""
|
||||||
|
var versionCode = 0
|
||||||
|
var screenWidth80 = 0
|
||||||
|
|
||||||
|
val Instance: AppGlobal get() = instance
|
||||||
|
val resources: Resources get() = Instance.resources
|
||||||
|
val packageManager: PackageManager get() = Instance.packageManager
|
||||||
|
|
||||||
|
var audioManager: AudioManager by Delegates.notNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.meloda.fast.common
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
|
|
||||||
|
|
||||||
object AppSettings {
|
|
||||||
|
|
||||||
val keyUseNavigationDrawer = booleanPreferencesKey("use_nav_drawer")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
|
||||||
name = "settings",
|
|
||||||
corruptionHandler = null,
|
|
||||||
scope = CoroutineScope(Dispatchers.IO + Job())
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,39 +1,37 @@
|
|||||||
package com.meloda.fast.common
|
package com.meloda.fast.common
|
||||||
|
|
||||||
import com.github.terrakok.cicerone.androidx.FragmentScreen
|
import com.github.terrakok.cicerone.androidx.FragmentScreen
|
||||||
import com.meloda.fast.api.model.VkConversation
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
|
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||||
import com.meloda.fast.model.UpdateItem
|
import com.meloda.fast.model.UpdateItem
|
||||||
|
import com.meloda.fast.screens.chatinfo.ChatInfoFragment
|
||||||
import com.meloda.fast.screens.conversations.ConversationsFragment
|
import com.meloda.fast.screens.conversations.ConversationsFragment
|
||||||
import com.meloda.fast.screens.login.LoginFragment
|
import com.meloda.fast.screens.login.LoginFragment
|
||||||
import com.meloda.fast.screens.main.MainFragment
|
import com.meloda.fast.screens.main.MainFragment
|
||||||
import com.meloda.fast.screens.messages.ForwardedMessagesFragment
|
import com.meloda.fast.screens.messages.ForwardedMessagesFragment
|
||||||
import com.meloda.fast.screens.messages.MessagesHistoryFragment
|
import com.meloda.fast.screens.messages.MessagesHistoryFragment
|
||||||
import com.meloda.fast.screens.settings.SettingsRootFragment
|
import com.meloda.fast.screens.settings.SettingsFragment
|
||||||
import com.meloda.fast.screens.updates.UpdatesFragment
|
import com.meloda.fast.screens.updates.UpdatesFragment
|
||||||
|
import com.meloda.fast.screens.userbanned.UserBannedFragment
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
@Suppress("FunctionName")
|
||||||
object Screens {
|
object Screens {
|
||||||
fun Main() = FragmentScreen { MainFragment() }
|
fun Main() = FragmentScreen { MainFragment.newInstance() }
|
||||||
|
|
||||||
fun Login(
|
fun Login() = FragmentScreen { LoginFragment.newInstance() }
|
||||||
getFastToken: Boolean = false
|
|
||||||
) = FragmentScreen {
|
|
||||||
LoginFragment.newInstance(getFastToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Conversations() = FragmentScreen { ConversationsFragment() }
|
fun Conversations() = FragmentScreen { ConversationsFragment() }
|
||||||
|
|
||||||
fun MessagesHistory(
|
fun MessagesHistory(
|
||||||
conversation: VkConversation,
|
conversation: VkConversationDomain,
|
||||||
user: VkUser?,
|
user: VkUser?,
|
||||||
group: VkGroup?
|
group: VkGroup?
|
||||||
) = FragmentScreen { MessagesHistoryFragment.newInstance(conversation, user, group) }
|
) = FragmentScreen { MessagesHistoryFragment.newInstance(conversation, user, group) }
|
||||||
|
|
||||||
fun ForwardedMessages(
|
fun ForwardedMessages(
|
||||||
conversation: VkConversation,
|
conversation: VkConversationDomain,
|
||||||
messages: List<VkMessage>,
|
messages: List<VkMessage>,
|
||||||
profiles: HashMap<Int, VkUser> = hashMapOf(),
|
profiles: HashMap<Int, VkUser> = hashMapOf(),
|
||||||
groups: HashMap<Int, VkGroup> = hashMapOf()
|
groups: HashMap<Int, VkGroup> = hashMapOf()
|
||||||
@@ -43,8 +41,25 @@ object Screens {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ChatInfo(
|
||||||
|
conversation: VkConversationDomain,
|
||||||
|
user: VkUser?,
|
||||||
|
group: VkGroup?
|
||||||
|
) = FragmentScreen { ChatInfoFragment.newInstance(conversation, user, group) }
|
||||||
|
|
||||||
fun Updates(updateItem: UpdateItem? = null) =
|
fun Updates(updateItem: UpdateItem? = null) =
|
||||||
FragmentScreen { UpdatesFragment.newInstance(updateItem) }
|
FragmentScreen { UpdatesFragment.newInstance(updateItem) }
|
||||||
|
|
||||||
fun Settings() = FragmentScreen { SettingsRootFragment() }
|
fun Settings() = FragmentScreen { SettingsFragment.newInstance() }
|
||||||
|
|
||||||
|
fun UserBanned(
|
||||||
|
memberName: String,
|
||||||
|
message: String,
|
||||||
|
restoreUrl: String,
|
||||||
|
accessToken: String
|
||||||
|
) = FragmentScreen {
|
||||||
|
UserBannedFragment.newInstance(
|
||||||
|
memberName, message, restoreUrl, accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,39 @@
|
|||||||
package com.meloda.fast.common
|
package com.meloda.fast.common
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.meloda.fast.BuildConfig
|
import com.meloda.fast.BuildConfig
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
import com.meloda.fast.api.network.ApiAnswer
|
import com.meloda.fast.api.network.ApiAnswer
|
||||||
import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse
|
import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse
|
||||||
import com.meloda.fast.data.ota.OtaApi
|
import com.meloda.fast.data.ota.OtaApi
|
||||||
import com.meloda.fast.extensions.setIfNotEquals
|
|
||||||
import com.meloda.fast.model.UpdateActualUrl
|
import com.meloda.fast.model.UpdateActualUrl
|
||||||
import com.meloda.fast.model.UpdateItem
|
import com.meloda.fast.model.UpdateItem
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class UpdateManager(private val repo: OtaApi) : CoroutineScope {
|
interface UpdateManager {
|
||||||
|
val stateFlow: Flow<UpdateManagerState>
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext
|
fun checkUpdates(): Job
|
||||||
get() = Dispatchers.Default
|
}
|
||||||
|
|
||||||
companion object {
|
class UpdateManagerImpl(private val repo: OtaApi) : UpdateManager {
|
||||||
val newUpdate = MutableLiveData<UpdateItem?>(null)
|
|
||||||
val updateError = MutableLiveData<Throwable?>(null)
|
|
||||||
|
|
||||||
var otaBaseUrl: String? = null
|
private val coroutineContext: CoroutineContext
|
||||||
private set
|
get() = Dispatchers.IO
|
||||||
}
|
|
||||||
|
|
||||||
private var listener: ((item: UpdateItem?, error: Throwable?) -> Unit)? = null
|
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||||
|
|
||||||
private fun getActualUrl() = launch {
|
private var otaBaseUrl: String? = null
|
||||||
|
|
||||||
|
override val stateFlow = MutableStateFlow(UpdateManagerState.EMPTY)
|
||||||
|
|
||||||
|
override fun checkUpdates() = coroutineScope.launch {
|
||||||
val job: suspend () -> ApiAnswer<UpdateActualUrl> = { repo.getActualUrl() }
|
val job: suspend () -> ApiAnswer<UpdateActualUrl> = { repo.getActualUrl() }
|
||||||
|
|
||||||
when (val jobResponse = job()) {
|
when (val jobResponse = job()) {
|
||||||
@@ -44,47 +46,55 @@ class UpdateManager(private val repo: OtaApi) : CoroutineScope {
|
|||||||
is ApiAnswer.Error -> {
|
is ApiAnswer.Error -> {
|
||||||
otaBaseUrl = null
|
otaBaseUrl = null
|
||||||
val throwable = jobResponse.error.throwable
|
val throwable = jobResponse.error.throwable
|
||||||
listener?.invoke(null, throwable)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
val newForm = stateFlow.value.copy(
|
||||||
updateError.setIfNotEquals(throwable)
|
updateItem = null,
|
||||||
}
|
throwable = throwable
|
||||||
|
)
|
||||||
|
stateFlow.emit(newForm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLatestRelease() = launch {
|
private fun getLatestRelease() = coroutineScope.launch {
|
||||||
val url = "$otaBaseUrl/releases-latest"
|
val url = "$otaBaseUrl/releases-latest"
|
||||||
|
|
||||||
val job: suspend () -> ApiAnswer<ApiResponse<OtaGetLatestReleaseResponse>> = {
|
val job: suspend () -> ApiAnswer<ApiResponse<OtaGetLatestReleaseResponse>> = {
|
||||||
repo.getLatestRelease(url = url, secretCode = getOtaSecret())
|
repo.getLatestRelease(url = url, secretCode = getOtaSecret())
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
when (val jobResponse = job()) {
|
||||||
when (val jobResponse = job()) {
|
is ApiAnswer.Success -> {
|
||||||
is ApiAnswer.Success -> {
|
val response = jobResponse.data.response ?: return@launch
|
||||||
val response = jobResponse.data.response ?: return@withContext
|
val latestRelease = response.release
|
||||||
val latestRelease = response.release
|
|
||||||
|
|
||||||
if (latestRelease != null &&
|
val updateItem = if (latestRelease != null &&
|
||||||
(AppGlobal.versionName
|
(AppGlobal.versionName
|
||||||
.split("_")
|
.split("_")
|
||||||
.getOrNull(1) != latestRelease.versionName ||
|
.getOrNull(1) != latestRelease.versionName ||
|
||||||
AppGlobal.versionCode < latestRelease.versionCode)
|
AppGlobal.versionCode < latestRelease.versionCode)
|
||||||
) {
|
) {
|
||||||
newUpdate.setIfNotEquals(latestRelease)
|
latestRelease
|
||||||
listener?.invoke(latestRelease, null)
|
} else {
|
||||||
} else {
|
null
|
||||||
newUpdate.setIfNotEquals(null)
|
|
||||||
listener?.invoke(null, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ApiAnswer.Error -> {
|
val newForm = stateFlow.value.copy(
|
||||||
val throwable = jobResponse.error.throwable
|
updateItem = updateItem,
|
||||||
updateError.setIfNotEquals(throwable)
|
throwable = null
|
||||||
listener?.invoke(null, throwable)
|
)
|
||||||
}
|
|
||||||
|
stateFlow.emit(newForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ApiAnswer.Error -> {
|
||||||
|
val throwable = jobResponse.error.throwable
|
||||||
|
|
||||||
|
val newForm = stateFlow.value.copy(
|
||||||
|
updateItem = null,
|
||||||
|
throwable = throwable
|
||||||
|
)
|
||||||
|
stateFlow.emit(newForm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,9 +102,15 @@ class UpdateManager(private val repo: OtaApi) : CoroutineScope {
|
|||||||
private fun getOtaSecret(): String {
|
private fun getOtaSecret(): String {
|
||||||
return URLEncoder.encode(BuildConfig.otaSecretCode, "utf-8")
|
return URLEncoder.encode(BuildConfig.otaSecretCode, "utf-8")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkUpdates(block: ((item: UpdateItem?, error: Throwable?) -> Unit)? = null) = launch {
|
data class UpdateManagerState(
|
||||||
this@UpdateManager.listener = block
|
val updateItem: UpdateItem?,
|
||||||
getActualUrl()
|
val throwable: Throwable?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val EMPTY = UpdateManagerState(
|
||||||
|
updateItem = null, throwable = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.meloda.fast.common.di
|
||||||
|
|
||||||
|
import com.meloda.fast.di.apiModule
|
||||||
|
import com.meloda.fast.di.dataModule
|
||||||
|
import com.meloda.fast.di.databaseModule
|
||||||
|
import com.meloda.fast.di.navigationModule
|
||||||
|
import com.meloda.fast.di.networkModule
|
||||||
|
import com.meloda.fast.di.otaModule
|
||||||
|
import com.meloda.fast.screens.captcha.di.captchaModule
|
||||||
|
import com.meloda.fast.screens.chatinfo.di.chatInfoModule
|
||||||
|
import com.meloda.fast.screens.conversations.di.conversationsModule
|
||||||
|
import com.meloda.fast.screens.login.di.loginModule
|
||||||
|
import com.meloda.fast.screens.main.di.mainModule
|
||||||
|
import com.meloda.fast.screens.messages.di.messagesHistoryModule
|
||||||
|
import com.meloda.fast.screens.photos.di.photoViewModule
|
||||||
|
import com.meloda.fast.screens.settings.di.settingsModule
|
||||||
|
import com.meloda.fast.screens.twofa.di.twoFaModule
|
||||||
|
import com.meloda.fast.screens.updates.di.updatesModule
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val applicationModule = module {
|
||||||
|
includes(
|
||||||
|
navigationModule,
|
||||||
|
databaseModule,
|
||||||
|
dataModule,
|
||||||
|
otaModule,
|
||||||
|
networkModule,
|
||||||
|
apiModule,
|
||||||
|
loginModule,
|
||||||
|
twoFaModule,
|
||||||
|
captchaModule,
|
||||||
|
mainModule,
|
||||||
|
conversationsModule,
|
||||||
|
chatInfoModule,
|
||||||
|
settingsModule,
|
||||||
|
updatesModule,
|
||||||
|
messagesHistoryModule,
|
||||||
|
photoViewModule,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package com.meloda.fast.compose
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.meloda.fast.ext.getString
|
||||||
|
import com.meloda.fast.model.base.UiText
|
||||||
|
import com.meloda.fast.ui.AppTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MaterialDialog(
|
||||||
|
onDismissAction: (() -> Unit),
|
||||||
|
title: UiText? = null,
|
||||||
|
message: UiText? = null,
|
||||||
|
positiveText: UiText? = null,
|
||||||
|
positiveAction: (() -> Unit)? = null,
|
||||||
|
negativeText: UiText? = null,
|
||||||
|
negativeAction: (() -> Unit)? = null,
|
||||||
|
neutralText: UiText? = null,
|
||||||
|
neutralAction: (() -> Unit)? = null,
|
||||||
|
content: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
var isVisible by remember {
|
||||||
|
mutableStateOf(true)
|
||||||
|
}
|
||||||
|
val onDismissRequest = {
|
||||||
|
onDismissAction.invoke()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
// TODO: 08.04.2023, Danil Nikolaev: implement animation
|
||||||
|
AlertAnimation(visible = isVisible) {
|
||||||
|
Dialog(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(
|
||||||
|
start = 20.dp,
|
||||||
|
top = 20.dp,
|
||||||
|
end = 20.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
title?.getString()?.let { title ->
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canScrollBackward) {
|
||||||
|
Divider(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f, fill = false)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row {
|
||||||
|
message?.getString()?.let { message ->
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
content?.let { content ->
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
content.invoke()
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canScrollForward) {
|
||||||
|
Divider(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
neutralText?.getString()?.let { text ->
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
neutralAction?.invoke()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
negativeText?.getString()?.let { text ->
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
negativeAction?.invoke()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
|
||||||
|
positiveText?.getString()?.let { text ->
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
positiveAction?.invoke()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,4 +15,7 @@ interface AccountsDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(values: List<AppAccount>)
|
suspend fun insert(values: List<AppAccount>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM accounts WHERE userId = :userId")
|
||||||
|
suspend fun deleteById(userId: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,15 +4,15 @@ import androidx.room.Dao
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.meloda.fast.api.model.VkConversation
|
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ConversationsDao {
|
interface ConversationsDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM conversations")
|
@Query("SELECT * FROM conversations")
|
||||||
suspend fun getAll(): List<VkConversation>
|
suspend fun getAll(): List<VkConversationDomain>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(values: List<VkConversation>)
|
suspend fun insert(values: List<VkConversationDomain>)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.meloda.fast.data.conversations
|
package com.meloda.fast.data.conversations
|
||||||
|
|
||||||
import com.meloda.fast.api.model.VkConversation
|
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||||
import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest
|
import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest
|
||||||
import com.meloda.fast.api.network.conversations.ConversationsGetRequest
|
import com.meloda.fast.api.network.conversations.ConversationsGetRequest
|
||||||
import com.meloda.fast.api.network.conversations.ConversationsPinRequest
|
import com.meloda.fast.api.network.conversations.ConversationsPinRequest
|
||||||
import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest
|
import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
|
|
||||||
class ConversationsRepository(
|
class ConversationsRepository(
|
||||||
private val conversationsApi: ConversationsApi,
|
private val conversationsApi: ConversationsApi,
|
||||||
@@ -20,6 +19,6 @@ class ConversationsRepository(
|
|||||||
|
|
||||||
suspend fun unpin(params: ConversationsUnpinRequest) = conversationsApi.unpin(params.map)
|
suspend fun unpin(params: ConversationsUnpinRequest) = conversationsApi.unpin(params.map)
|
||||||
|
|
||||||
suspend fun store(conversations: List<VkConversation>) = conversationsDao.insert(conversations)
|
suspend fun store(conversations: List<VkConversationDomain>) = conversationsDao.insert(conversations)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.meloda.fast.data.messages
|
package com.meloda.fast.data.messages
|
||||||
|
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
|
import com.meloda.fast.api.model.base.BaseVkChat
|
||||||
import com.meloda.fast.api.model.base.BaseVkLongPoll
|
import com.meloda.fast.api.model.base.BaseVkLongPoll
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||||
import com.meloda.fast.api.network.ApiAnswer
|
import com.meloda.fast.api.network.ApiAnswer
|
||||||
import com.meloda.fast.api.network.messages.MessagesGetByIdResponse
|
import com.meloda.fast.api.network.messages.MessagesGetByIdResponse
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesGetConversationMembersResponse
|
||||||
import com.meloda.fast.api.network.messages.MessagesGetHistoryResponse
|
import com.meloda.fast.api.network.messages.MessagesGetHistoryResponse
|
||||||
import com.meloda.fast.api.network.messages.MessagesUrls
|
import com.meloda.fast.api.network.messages.MessagesUrls
|
||||||
import retrofit2.http.FieldMap
|
import retrofit2.http.FieldMap
|
||||||
@@ -53,4 +55,16 @@ interface MessagesApi {
|
|||||||
@POST(MessagesUrls.MarkAsRead)
|
@POST(MessagesUrls.MarkAsRead)
|
||||||
suspend fun markAsRead(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Int>>
|
suspend fun markAsRead(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Int>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MessagesUrls.GetChat)
|
||||||
|
suspend fun getChat(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<BaseVkChat>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MessagesUrls.GetConversationMembers)
|
||||||
|
suspend fun getConversationMembers(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<MessagesGetConversationMembersResponse>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MessagesUrls.RemoveChatUser)
|
||||||
|
suspend fun removeChatUser(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Int>>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,28 @@ package com.meloda.fast.data.messages
|
|||||||
|
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
|
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesDeleteRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesEditRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesGetChatRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesGetConversationMembersRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesGetHistoryRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesMarkAsImportantRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesPinMessageRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesRemoveChatUserRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesSendRequest
|
||||||
|
import com.meloda.fast.api.network.messages.MessagesUnPinMessageRequest
|
||||||
import com.meloda.fast.data.longpoll.LongPollApi
|
import com.meloda.fast.data.longpoll.LongPollApi
|
||||||
import com.meloda.fast.api.network.messages.*
|
|
||||||
|
|
||||||
class MessagesRepository(
|
class MessagesRepository(
|
||||||
private val messagesApi: MessagesApi,
|
private val messagesApi: MessagesApi,
|
||||||
private val messagesDao: MessagesDao,
|
private val messagesDao: MessagesDao,
|
||||||
private val longPollApi: LongPollApi
|
private val longPollApi: LongPollApi,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
suspend fun store(message: VkMessage) = store(listOf(message))
|
||||||
|
|
||||||
suspend fun store(messages: List<VkMessage>) = messagesDao.insert(messages)
|
suspend fun store(messages: List<VkMessage>) = messagesDao.insert(messages)
|
||||||
|
|
||||||
suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
|
suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
|
||||||
@@ -41,7 +54,7 @@ class MessagesRepository(
|
|||||||
|
|
||||||
suspend fun getLongPollUpdates(
|
suspend fun getLongPollUpdates(
|
||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
params: LongPollGetUpdatesRequest
|
params: LongPollGetUpdatesRequest,
|
||||||
) = longPollApi.getResponse(serverUrl, params.map)
|
) = longPollApi.getResponse(serverUrl, params.map)
|
||||||
|
|
||||||
suspend fun getById(params: MessagesGetByIdRequest) =
|
suspend fun getById(params: MessagesGetByIdRequest) =
|
||||||
@@ -50,7 +63,7 @@ class MessagesRepository(
|
|||||||
suspend fun markAsRead(
|
suspend fun markAsRead(
|
||||||
peerId: Int,
|
peerId: Int,
|
||||||
messagesIds: List<Int>? = null,
|
messagesIds: List<Int>? = null,
|
||||||
startMessageId: Int? = null
|
startMessageId: Int? = null,
|
||||||
) = messagesApi.markAsRead(
|
) = messagesApi.markAsRead(
|
||||||
mutableMapOf("peer_id" to peerId.toString()).apply {
|
mutableMapOf("peer_id" to peerId.toString()).apply {
|
||||||
messagesIds?.let {
|
messagesIds?.let {
|
||||||
@@ -62,4 +75,30 @@ class MessagesRepository(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
suspend fun getChat(
|
||||||
|
chatId: Int,
|
||||||
|
fields: String? = null,
|
||||||
|
) = messagesApi.getChat(MessagesGetChatRequest(chatId, fields).map)
|
||||||
|
|
||||||
|
suspend fun getConversationMembers(
|
||||||
|
peerId: Int,
|
||||||
|
offset: Int? = null,
|
||||||
|
count: Int? = null,
|
||||||
|
extended: Boolean? = null,
|
||||||
|
fields: String? = null,
|
||||||
|
) = messagesApi.getConversationMembers(
|
||||||
|
MessagesGetConversationMembersRequest(
|
||||||
|
peerId,
|
||||||
|
offset,
|
||||||
|
count,
|
||||||
|
extended,
|
||||||
|
fields
|
||||||
|
).map
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun removeChatUser(
|
||||||
|
chatId: Int,
|
||||||
|
memberId: Int,
|
||||||
|
) = messagesApi.removeChatUser(MessagesRemoveChatUserRequest(chatId, memberId).map)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.meloda.fast.database
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.meloda.fast.data.account.AccountsDao
|
||||||
|
import com.meloda.fast.model.AppAccount
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [AppAccount::class],
|
||||||
|
version = 1,
|
||||||
|
exportSchema = false
|
||||||
|
)
|
||||||
|
abstract class AccountsDatabase : RoomDatabase() {
|
||||||
|
abstract val accountsDao: AccountsDao
|
||||||
|
}
|
||||||
+5
-13
@@ -1,38 +1,30 @@
|
|||||||
package com.meloda.fast.database
|
package com.meloda.fast.database
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.meloda.fast.api.model.VkConversation
|
|
||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
import com.meloda.fast.data.account.AccountsDao
|
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||||
import com.meloda.fast.data.conversations.ConversationsDao
|
import com.meloda.fast.data.conversations.ConversationsDao
|
||||||
import com.meloda.fast.data.groups.GroupsDao
|
import com.meloda.fast.data.groups.GroupsDao
|
||||||
import com.meloda.fast.data.messages.MessagesDao
|
import com.meloda.fast.data.messages.MessagesDao
|
||||||
import com.meloda.fast.data.users.UsersDao
|
import com.meloda.fast.data.users.UsersDao
|
||||||
import com.meloda.fast.model.AppAccount
|
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
AppAccount::class,
|
VkConversationDomain::class,
|
||||||
VkConversation::class,
|
|
||||||
VkMessage::class,
|
VkMessage::class,
|
||||||
VkUser::class,
|
VkUser::class,
|
||||||
VkGroup::class
|
VkGroup::class
|
||||||
],
|
],
|
||||||
version = 34,
|
version = 42,
|
||||||
exportSchema = true,
|
exportSchema = false
|
||||||
autoMigrations = [
|
|
||||||
AutoMigration(from = 33, to = 34)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class CacheDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract val accountsDao: AccountsDao
|
|
||||||
abstract val conversationsDao: ConversationsDao
|
abstract val conversationsDao: ConversationsDao
|
||||||
abstract val messagesDao: MessagesDao
|
abstract val messagesDao: MessagesDao
|
||||||
abstract val usersDao: UsersDao
|
abstract val usersDao: UsersDao
|
||||||
@@ -2,6 +2,7 @@ package com.meloda.fast.database
|
|||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.meloda.fast.api.base.AttachmentClassNameIsEmptyException
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||||
@@ -18,25 +19,37 @@ class Converters {
|
|||||||
fun fromGeoToString(geo: BaseVkMessage.Geo?): String? {
|
fun fromGeoToString(geo: BaseVkMessage.Geo?): String? {
|
||||||
if (geo == null) return null
|
if (geo == null) return null
|
||||||
|
|
||||||
val string = Gson().toJson(geo)
|
return try {
|
||||||
|
val string = Gson().toJson(geo)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromStringToGeo(string: String?): BaseVkMessage.Geo? {
|
fun fromStringToGeo(string: String?): BaseVkMessage.Geo? {
|
||||||
if (string == null) return null
|
if (string == null) return null
|
||||||
|
|
||||||
val geo = Gson().fromJson(string, BaseVkMessage.Geo::class.java)
|
return try {
|
||||||
|
val geo = Gson().fromJson(string, BaseVkMessage.Geo::class.java)
|
||||||
|
|
||||||
return geo
|
return geo
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
|
fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
|
||||||
if (messages == null) return null
|
if (messages == null) return null
|
||||||
|
|
||||||
val string = messages.map { fromVkMessageToString(it)!! }.joinToString { CACHE_SEPARATOR }
|
val string = messages
|
||||||
|
.mapNotNull(::fromVkMessageToString)
|
||||||
|
.joinToString(separator = CACHE_SEPARATOR)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
@@ -46,40 +59,52 @@ class Converters {
|
|||||||
if (string == null) return null
|
if (string == null) return null
|
||||||
|
|
||||||
if (string.contains(CACHE_SEPARATOR)) {
|
if (string.contains(CACHE_SEPARATOR)) {
|
||||||
val messages =
|
val messages = string
|
||||||
string.split(CACHE_SEPARATOR).map { fromStringToVkMessage(it)!! }
|
.split(CACHE_SEPARATOR)
|
||||||
|
.mapNotNull(::fromStringToVkMessage)
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val message = fromStringToVkMessage(string)!!
|
val message = fromStringToVkMessage(string)
|
||||||
|
return message?.let { listOf(it) }
|
||||||
return listOf(message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromVkMessageToString(message: VkMessage?): String? {
|
fun fromVkMessageToString(message: VkMessage?): String? {
|
||||||
if (message == null) return null
|
if (message == null) return null
|
||||||
|
|
||||||
return Gson().toJson(message)
|
return try {
|
||||||
|
val string = Gson().toJson(message)
|
||||||
|
|
||||||
|
return string
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromStringToVkMessage(string: String?): VkMessage? {
|
fun fromStringToVkMessage(string: String?): VkMessage? {
|
||||||
if (string == null) return null
|
if (string == null) return null
|
||||||
|
|
||||||
val message = Gson().fromJson(string, VkMessage::class.java)
|
return try {
|
||||||
|
val message = Gson().fromJson(string, VkMessage::class.java)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromListVkAttachmentToString(attachments: List<VkAttachment>?): String? {
|
fun fromListVkAttachmentToString(attachments: List<VkAttachment>?): String? {
|
||||||
if (attachments == null) return null
|
if (attachments == null) return null
|
||||||
|
|
||||||
val string =
|
val string = attachments
|
||||||
attachments.map { fromVkAttachmentToString(it)!! }.joinToString { CACHE_SEPARATOR }
|
.mapNotNull(::fromVkAttachmentToString)
|
||||||
|
.joinToString(separator = CACHE_SEPARATOR)
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,34 +113,48 @@ class Converters {
|
|||||||
if (string == null) return null
|
if (string == null) return null
|
||||||
|
|
||||||
if (string.contains(CACHE_SEPARATOR)) {
|
if (string.contains(CACHE_SEPARATOR)) {
|
||||||
val attachments =
|
val attachments = string
|
||||||
string.split(CACHE_SEPARATOR).map { fromStringToVkAttachment(it)!! }
|
.split(CACHE_SEPARATOR)
|
||||||
|
.mapNotNull(::fromStringToVkAttachment)
|
||||||
return attachments
|
return attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val attachment = fromStringToVkAttachment(string)
|
||||||
|
|
||||||
val attachment = fromStringToVkAttachment(string)!!
|
return attachment?.let { listOf(it) }
|
||||||
|
|
||||||
return listOf(attachment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromVkAttachmentToString(attachment: VkAttachment?): String? {
|
fun fromVkAttachmentToString(attachment: VkAttachment?): String? {
|
||||||
if (attachment == null) return null
|
if (attachment == null) return null
|
||||||
|
|
||||||
val string = Gson().toJson(attachment)
|
try {
|
||||||
|
attachment.javaClass.getDeclaredField("className")
|
||||||
return string
|
} catch (e: NoSuchFieldException) {
|
||||||
|
throw AttachmentClassNameIsEmptyException(attachment)
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val string = Gson().toJson(attachment)
|
||||||
|
string
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromStringToVkAttachment(string: String?): VkAttachment? {
|
fun fromStringToVkAttachment(string: String?): VkAttachment? {
|
||||||
if (string == null) return null
|
if (string.isNullOrBlank()) return null
|
||||||
|
|
||||||
val className = JSONObject(string).optString("className")
|
return try {
|
||||||
|
val className = JSONObject(string).optString("className")
|
||||||
|
|
||||||
val attachment = Gson().fromJson(string, Class.forName(className)) as? VkAttachment?
|
val attachment = Gson().fromJson(string, Class.forName(className)) as? VkAttachment?
|
||||||
|
|
||||||
return attachment
|
return attachment
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.meloda.fast.di
|
||||||
|
|
||||||
|
import com.meloda.fast.api.longpoll.LongPollUpdatesParser
|
||||||
|
import com.meloda.fast.data.account.AccountApi
|
||||||
|
import com.meloda.fast.data.audios.AudiosApi
|
||||||
|
import com.meloda.fast.data.auth.AuthApi
|
||||||
|
import com.meloda.fast.data.conversations.ConversationsApi
|
||||||
|
import com.meloda.fast.data.files.FilesApi
|
||||||
|
import com.meloda.fast.data.longpoll.LongPollApi
|
||||||
|
import com.meloda.fast.data.messages.MessagesApi
|
||||||
|
import com.meloda.fast.data.ota.OtaApi
|
||||||
|
import com.meloda.fast.data.photos.PhotosApi
|
||||||
|
import com.meloda.fast.data.users.UsersApi
|
||||||
|
import com.meloda.fast.data.videos.VideosApi
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val apiModule = module {
|
||||||
|
single { api(AuthApi::class.java) }
|
||||||
|
single { api(ConversationsApi::class.java) }
|
||||||
|
single { api(UsersApi::class.java) }
|
||||||
|
single { api(MessagesApi::class.java) }
|
||||||
|
single { api(LongPollApi::class.java) }
|
||||||
|
single { api(AccountApi::class.java) }
|
||||||
|
single { api(OtaApi::class.java) }
|
||||||
|
single { api(PhotosApi::class.java) }
|
||||||
|
single { api(VideosApi::class.java) }
|
||||||
|
single { api(AudiosApi::class.java) }
|
||||||
|
single { api(FilesApi::class.java) }
|
||||||
|
|
||||||
|
singleOf(::LongPollUpdatesParser)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T> Scope.api(className: Class<T>): T = retrofit().create(className)
|
||||||
@@ -1,103 +1,26 @@
|
|||||||
package com.meloda.fast.di
|
package com.meloda.fast.di
|
||||||
|
|
||||||
import com.meloda.fast.data.longpoll.LongPollApi
|
|
||||||
import com.meloda.fast.data.account.AccountApi
|
|
||||||
import com.meloda.fast.data.account.AccountsDao
|
|
||||||
import com.meloda.fast.data.account.AccountsRepository
|
import com.meloda.fast.data.account.AccountsRepository
|
||||||
import com.meloda.fast.data.audios.AudiosApi
|
|
||||||
import com.meloda.fast.data.audios.AudiosRepository
|
import com.meloda.fast.data.audios.AudiosRepository
|
||||||
import com.meloda.fast.data.auth.AuthApi
|
|
||||||
import com.meloda.fast.data.auth.AuthRepository
|
import com.meloda.fast.data.auth.AuthRepository
|
||||||
import com.meloda.fast.data.conversations.ConversationsApi
|
|
||||||
import com.meloda.fast.data.conversations.ConversationsDao
|
|
||||||
import com.meloda.fast.data.conversations.ConversationsRepository
|
import com.meloda.fast.data.conversations.ConversationsRepository
|
||||||
import com.meloda.fast.data.files.FilesApi
|
|
||||||
import com.meloda.fast.data.files.FilesRepository
|
import com.meloda.fast.data.files.FilesRepository
|
||||||
import com.meloda.fast.data.groups.GroupsDao
|
|
||||||
import com.meloda.fast.data.groups.GroupsRepository
|
|
||||||
import com.meloda.fast.data.messages.MessagesApi
|
|
||||||
import com.meloda.fast.data.messages.MessagesDao
|
|
||||||
import com.meloda.fast.data.messages.MessagesRepository
|
import com.meloda.fast.data.messages.MessagesRepository
|
||||||
import com.meloda.fast.data.photos.PhotosApi
|
|
||||||
import com.meloda.fast.data.photos.PhotosRepository
|
import com.meloda.fast.data.photos.PhotosRepository
|
||||||
import com.meloda.fast.data.users.UsersApi
|
|
||||||
import com.meloda.fast.data.users.UsersDao
|
|
||||||
import com.meloda.fast.data.users.UsersRepository
|
import com.meloda.fast.data.users.UsersRepository
|
||||||
import com.meloda.fast.data.videos.VideosApi
|
|
||||||
import com.meloda.fast.data.videos.VideosRepository
|
import com.meloda.fast.data.videos.VideosRepository
|
||||||
import dagger.Module
|
import org.koin.core.module.dsl.singleOf
|
||||||
import dagger.Provides
|
import org.koin.dsl.module
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
@Module
|
|
||||||
object DataModule {
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideConversationsRepository(
|
|
||||||
conversationsApi: ConversationsApi,
|
|
||||||
conversationsDao: ConversationsDao
|
|
||||||
): ConversationsRepository = ConversationsRepository(conversationsApi, conversationsDao)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideMessagesRepository(
|
|
||||||
messagesApi: MessagesApi,
|
|
||||||
messagesDao: MessagesDao,
|
|
||||||
longPollApi: LongPollApi
|
|
||||||
): MessagesRepository = MessagesRepository(messagesApi, messagesDao, longPollApi)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideUsersRepository(
|
|
||||||
usersApi: UsersApi,
|
|
||||||
usersDao: UsersDao
|
|
||||||
): UsersRepository = UsersRepository(usersApi, usersDao)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideGroupsRepository(
|
|
||||||
groupsDao: GroupsDao
|
|
||||||
): GroupsRepository = GroupsRepository(groupsDao)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideAuthRepository(
|
|
||||||
authApi: AuthApi
|
|
||||||
): AuthRepository = AuthRepository(authApi)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideAccountsRepository(
|
|
||||||
accountApi: AccountApi,
|
|
||||||
accountsDao: AccountsDao
|
|
||||||
): AccountsRepository = AccountsRepository(accountApi, accountsDao)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun providePhotosRepository(
|
|
||||||
photosApi: PhotosApi
|
|
||||||
): PhotosRepository = PhotosRepository(photosApi)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideVideosRepository(
|
|
||||||
videosApi: VideosApi
|
|
||||||
): VideosRepository = VideosRepository(videosApi)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideAudiosRepository(
|
|
||||||
audiosApi: AudiosApi
|
|
||||||
): AudiosRepository = AudiosRepository(audiosApi)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideFilesRepository(
|
|
||||||
filesApi: FilesApi
|
|
||||||
): FilesRepository = FilesRepository(filesApi)
|
|
||||||
|
|
||||||
|
// TODO: 17.04.2023, Danil Nikolaev: use specific repositories in local DI modules
|
||||||
|
val dataModule = module {
|
||||||
|
singleOf(::ConversationsRepository)
|
||||||
|
singleOf(::MessagesRepository)
|
||||||
|
singleOf(::UsersRepository)
|
||||||
|
singleOf(::AuthRepository)
|
||||||
|
singleOf(::AccountsRepository)
|
||||||
|
singleOf(::PhotosRepository)
|
||||||
|
singleOf(::VideosRepository)
|
||||||
|
singleOf(::AudiosRepository)
|
||||||
|
singleOf(::FilesRepository)
|
||||||
}
|
}
|
||||||
@@ -1,50 +1,28 @@
|
|||||||
package com.meloda.fast.di
|
package com.meloda.fast.di
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
import com.meloda.fast.common.AppGlobal
|
import com.meloda.fast.common.AppGlobal
|
||||||
import com.meloda.fast.data.account.AccountsDao
|
import com.meloda.fast.database.AccountsDatabase
|
||||||
import com.meloda.fast.data.conversations.ConversationsDao
|
import com.meloda.fast.database.CacheDatabase
|
||||||
import com.meloda.fast.data.groups.GroupsDao
|
import org.koin.core.scope.Scope
|
||||||
import com.meloda.fast.data.messages.MessagesDao
|
import org.koin.dsl.module
|
||||||
import com.meloda.fast.data.users.UsersDao
|
|
||||||
import com.meloda.fast.database.AppDatabase
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
@Module
|
|
||||||
object DatabaseModule {
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAppDatabase(): AppDatabase =
|
|
||||||
AppGlobal.appDatabase
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAccountsDao(appDatabase: AppDatabase): AccountsDao =
|
|
||||||
appDatabase.accountsDao
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideConversationsDao(appDatabase: AppDatabase): ConversationsDao =
|
|
||||||
appDatabase.conversationsDao
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideMessagesDao(appDatabase: AppDatabase): MessagesDao =
|
|
||||||
appDatabase.messagesDao
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideUsersDao(appDatabase: AppDatabase): UsersDao =
|
|
||||||
appDatabase.usersDao
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideGroupsDao(appDatabase: AppDatabase): GroupsDao =
|
|
||||||
appDatabase.groupsDao
|
|
||||||
|
|
||||||
|
val databaseModule = module {
|
||||||
|
single {
|
||||||
|
Room.databaseBuilder(AppGlobal.Instance, CacheDatabase::class.java, "cache")
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
Room.databaseBuilder(AppGlobal.Instance, AccountsDatabase::class.java, "accounts")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
single { cache().conversationsDao }
|
||||||
|
single { cache().messagesDao }
|
||||||
|
single { cache().usersDao }
|
||||||
|
single { cache().groupsDao }
|
||||||
|
single { accounts().accountsDao }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Scope.cache(): CacheDatabase = get()
|
||||||
|
private fun Scope.accounts(): AccountsDatabase = get()
|
||||||
|
|||||||
@@ -2,24 +2,19 @@ package com.meloda.fast.di
|
|||||||
|
|
||||||
import com.github.terrakok.cicerone.Cicerone
|
import com.github.terrakok.cicerone.Cicerone
|
||||||
import com.github.terrakok.cicerone.Router
|
import com.github.terrakok.cicerone.Router
|
||||||
import dagger.Module
|
import com.meloda.fast.screens.captcha.screen.CaptchaScreen
|
||||||
import dagger.Provides
|
import com.meloda.fast.screens.twofa.screen.TwoFaScreen
|
||||||
import dagger.hilt.InstallIn
|
import org.koin.core.module.dsl.singleOf
|
||||||
import dagger.hilt.components.SingletonComponent
|
import org.koin.core.scope.Scope
|
||||||
import javax.inject.Singleton
|
import org.koin.dsl.module
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
val navigationModule = module {
|
||||||
@Module
|
single { Cicerone.create() }
|
||||||
object NavigationModule {
|
single { cicerone().router }
|
||||||
@Provides
|
single { cicerone().getNavigatorHolder() }
|
||||||
@Singleton
|
|
||||||
fun getCicerone(): Cicerone<Router> = Cicerone.create()
|
|
||||||
|
|
||||||
@Provides
|
singleOf(::CaptchaScreen)
|
||||||
@Singleton
|
singleOf(::TwoFaScreen)
|
||||||
fun getRouter(cicerone: Cicerone<Router>) = cicerone.router
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun getNavigationHolder(cicerone: Cicerone<Router>) = cicerone.getNavigatorHolder()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Scope.cicerone(): Cicerone<Router> = get()
|
||||||
|
|||||||
@@ -2,96 +2,34 @@ package com.meloda.fast.di
|
|||||||
|
|
||||||
import com.chuckerteam.chucker.api.ChuckerCollector
|
import com.chuckerteam.chucker.api.ChuckerCollector
|
||||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.meloda.fast.api.longpoll.LongPollUpdatesParser
|
|
||||||
import com.meloda.fast.api.network.AuthInterceptor
|
import com.meloda.fast.api.network.AuthInterceptor
|
||||||
import com.meloda.fast.api.network.ResultCallFactory
|
import com.meloda.fast.api.network.ResultCallFactory
|
||||||
import com.meloda.fast.api.network.VkUrls
|
import com.meloda.fast.api.network.VkUrls
|
||||||
import com.meloda.fast.common.AppGlobal
|
|
||||||
import com.meloda.fast.common.UpdateManager
|
|
||||||
import com.meloda.fast.data.account.AccountApi
|
|
||||||
import com.meloda.fast.data.audios.AudiosApi
|
|
||||||
import com.meloda.fast.data.auth.AuthApi
|
|
||||||
import com.meloda.fast.data.conversations.ConversationsApi
|
|
||||||
import com.meloda.fast.data.files.FilesApi
|
|
||||||
import com.meloda.fast.data.longpoll.LongPollApi
|
|
||||||
import com.meloda.fast.data.messages.MessagesApi
|
|
||||||
import com.meloda.fast.data.messages.MessagesRepository
|
|
||||||
import com.meloda.fast.data.ota.OtaApi
|
|
||||||
import com.meloda.fast.data.photos.PhotosApi
|
|
||||||
import com.meloda.fast.data.users.UsersApi
|
|
||||||
import com.meloda.fast.data.videos.VideosApi
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
import org.koin.dsl.module
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
val networkModule = module {
|
||||||
@Module
|
single { ChuckerCollector(get()) }
|
||||||
object NetworkModule {
|
single { ChuckerInterceptor.Builder(get()).collector(get()).build() }
|
||||||
|
singleOf(::AuthInterceptor)
|
||||||
/*
|
single { GsonBuilder().setLenient().create() }
|
||||||
|
single {
|
||||||
val chuckerCollector = ChuckerCollector(
|
|
||||||
context = this,
|
|
||||||
// Toggles visibility of the notification
|
|
||||||
showNotification = true,
|
|
||||||
// Allows to customize the retention period of collected data
|
|
||||||
retentionPeriod = RetentionManager.Period.ONE_HOUR
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create the Interceptor
|
|
||||||
val chuckerInterceptor = ChuckerInterceptor.Builder(context)
|
|
||||||
// The previously created Collector
|
|
||||||
.collector(chuckerCollector)
|
|
||||||
// The max body content length in bytes, after this responses will be truncated.
|
|
||||||
.maxContentLength(250_000L)
|
|
||||||
// List of headers to replace with ** in the Chucker UI
|
|
||||||
.redactHeaders("Auth-Token", "Bearer")
|
|
||||||
// Read the whole response body even when the client does not consume the response completely.
|
|
||||||
// This is useful in case of parsing errors or when the response body
|
|
||||||
// is closed before being read like in Retrofit with Void and Unit types.
|
|
||||||
.alwaysReadResponseBody(true)
|
|
||||||
// Use decoder when processing request and response bodies. When multiple decoders are installed they
|
|
||||||
// are applied in an order they were added.
|
|
||||||
.addBodyDecoder(decoder)
|
|
||||||
// Controls Android shortcut creation. Available in SNAPSHOTS versions only at the moment
|
|
||||||
.createShortcut(true)
|
|
||||||
.build()
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideChuckerCollector(): ChuckerCollector =
|
|
||||||
ChuckerCollector(AppGlobal.Instance)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideChuckerInterceptor(
|
|
||||||
chuckerCollector: ChuckerCollector
|
|
||||||
): ChuckerInterceptor =
|
|
||||||
ChuckerInterceptor.Builder(AppGlobal.Instance)
|
|
||||||
.collector(chuckerCollector)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideOkHttpClient(
|
|
||||||
authInterceptor: AuthInterceptor,
|
|
||||||
chuckerInterceptor: ChuckerInterceptor
|
|
||||||
): OkHttpClient =
|
|
||||||
OkHttpClient.Builder()
|
OkHttpClient.Builder()
|
||||||
.connectTimeout(20, TimeUnit.SECONDS)
|
.connectTimeout(20, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.addInterceptor(authInterceptor)
|
.addInterceptor(authInterceptor())
|
||||||
.addInterceptor(chuckerInterceptor)
|
.addInterceptor(
|
||||||
|
chuckerInterceptor().apply {
|
||||||
|
redactHeader("Secret-Code")
|
||||||
|
}
|
||||||
|
)
|
||||||
.followRedirects(true)
|
.followRedirects(true)
|
||||||
.followSslRedirects(true)
|
.followSslRedirects(true)
|
||||||
.addInterceptor(
|
.addInterceptor(
|
||||||
@@ -99,92 +37,17 @@ val chuckerInterceptor = ChuckerInterceptor.Builder(context)
|
|||||||
level = HttpLoggingInterceptor.Level.BODY
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
}
|
}
|
||||||
).build()
|
).build()
|
||||||
|
}
|
||||||
@Singleton
|
single {
|
||||||
@Provides
|
Retrofit.Builder()
|
||||||
fun provideGson(): Gson = GsonBuilder()
|
.baseUrl("${VkUrls.API}/")
|
||||||
.setLenient()
|
.addConverterFactory(GsonConverterFactory.create(get()))
|
||||||
.create()
|
.addCallAdapterFactory(ResultCallFactory(get()))
|
||||||
|
.client(get())
|
||||||
@Singleton
|
.build()
|
||||||
@Provides
|
}
|
||||||
fun provideRetrofit(
|
|
||||||
client: OkHttpClient,
|
|
||||||
gson: Gson
|
|
||||||
): Retrofit = Retrofit.Builder()
|
|
||||||
.baseUrl("${VkUrls.API}/")
|
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
|
||||||
.addCallAdapterFactory(ResultCallFactory())
|
|
||||||
.client(client)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor()
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAuthApi(retrofit: Retrofit): AuthApi =
|
|
||||||
retrofit.create(AuthApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideConversationsApi(retrofit: Retrofit): ConversationsApi =
|
|
||||||
retrofit.create(ConversationsApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideUsersApi(retrofit: Retrofit): UsersApi =
|
|
||||||
retrofit.create(UsersApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideMessagesApi(retrofit: Retrofit): MessagesApi =
|
|
||||||
retrofit.create(MessagesApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideLongPollApi(retrofit: Retrofit): LongPollApi =
|
|
||||||
retrofit.create(LongPollApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideLongPollUpdatesParser(messagesRepository: MessagesRepository): LongPollUpdatesParser =
|
|
||||||
LongPollUpdatesParser(messagesRepository)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAccountApi(retrofit: Retrofit): AccountApi =
|
|
||||||
retrofit.create(AccountApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideOtaApi(retrofit: Retrofit): OtaApi =
|
|
||||||
retrofit.create(OtaApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideUpdateManager(otaApi: OtaApi): UpdateManager =
|
|
||||||
UpdateManager(otaApi)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providePhotosApi(retrofit: Retrofit): PhotosApi =
|
|
||||||
retrofit.create(PhotosApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideVideosApi(retrofit: Retrofit): VideosApi =
|
|
||||||
retrofit.create(VideosApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAudiosApi(retrofit: Retrofit): AudiosApi =
|
|
||||||
retrofit.create(AudiosApi::class.java)
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideFilesApi(retrofit: Retrofit): FilesApi =
|
|
||||||
retrofit.create(FilesApi::class.java)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun Scope.retrofit(): Retrofit = get()
|
||||||
|
private fun Scope.authInterceptor(): AuthInterceptor = get()
|
||||||
|
private fun Scope.chuckerInterceptor(): ChuckerInterceptor = get()
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.meloda.fast.di
|
||||||
|
|
||||||
|
import com.meloda.fast.common.UpdateManager
|
||||||
|
import com.meloda.fast.common.UpdateManagerImpl
|
||||||
|
import com.meloda.fast.data.ota.OtaApi
|
||||||
|
import org.koin.core.module.dsl.bind
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val otaModule = module {
|
||||||
|
single { api(OtaApi::class.java) }
|
||||||
|
singleOf(::UpdateManagerImpl) { bind<UpdateManager>() }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
fun Activity.edgeToEdge() {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
context(AppCompatActivity)
|
||||||
|
fun <T> Flow<T>.listenValue(action: suspend (T) -> Unit) = listenValue(lifecycleScope, action)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= sdkInt) {
|
||||||
|
action?.invoke()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sdkAndUp(sdkInt: Int, action: () -> Unit): Boolean? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= sdkInt) {
|
||||||
|
action.invoke()
|
||||||
|
true
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSdkAtLeastOr(
|
||||||
|
sdkInt: Int,
|
||||||
|
action: (() -> Unit)? = null,
|
||||||
|
orAction: (() -> Unit)? = null
|
||||||
|
): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= sdkInt) {
|
||||||
|
action?.invoke()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
orAction?.invoke()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sdk26AndUp(action: () -> Unit): Boolean? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
action.invoke()
|
||||||
|
true
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sdk30AndUp(action: () -> Unit): Boolean? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
action.invoke()
|
||||||
|
true
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sdk33AndUp(action: () -> Unit): Boolean? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
action.invoke()
|
||||||
|
true
|
||||||
|
} else null
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
val Boolean?.isTrue: Boolean get() = this == true
|
||||||
|
|
||||||
|
val Boolean?.isFalse: Boolean get() = this == false
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST", "DEPRECATION")
|
||||||
|
fun <T : Parcelable> Bundle.getParcelableArrayListCompat(
|
||||||
|
key: String?,
|
||||||
|
clazz: Class<T>
|
||||||
|
): java.util.ArrayList<T>? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getParcelableArrayList(key, clazz)
|
||||||
|
} else {
|
||||||
|
getParcelableArrayList<Parcelable>(key) as ArrayList<T>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
fun <T : Parcelable> Bundle.getParcelableCompat(key: String?, clazz: Class<T>): T? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getParcelable(key, clazz)
|
||||||
|
} else {
|
||||||
|
getParcelable(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION", "UNCHECKED_CAST")
|
||||||
|
fun <T: Serializable> Bundle.getSerializableCompat(key: String?, clazz: Class<T>): T? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getSerializable(key, clazz)
|
||||||
|
} else {
|
||||||
|
getSerializable(key) as? T
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Indication
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import com.meloda.fast.common.AppGlobal
|
||||||
|
import com.meloda.fast.model.base.UiText
|
||||||
|
import com.meloda.fast.model.base.parseString
|
||||||
|
import com.meloda.fast.screens.settings.SettingsFragment
|
||||||
|
import com.meloda.fast.util.AndroidUtils
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
fun Modifier.clickableSound(
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClickLabel: String? = null,
|
||||||
|
role: Role? = null,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
): Modifier = this.clickable(
|
||||||
|
enabled = enabled,
|
||||||
|
onClickLabel = onClickLabel,
|
||||||
|
role = role,
|
||||||
|
onClick = {
|
||||||
|
AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK)
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
fun Modifier.combinedClickableSound(
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClickLabel: String? = null,
|
||||||
|
role: Role? = null,
|
||||||
|
onLongClickLabel: String? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
onDoubleClick: (() -> Unit)? = null,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
): Modifier = composed {
|
||||||
|
this.combinedClickableSound(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
enabled = enabled,
|
||||||
|
onClickLabel = onClickLabel,
|
||||||
|
role = role,
|
||||||
|
onLongClickLabel = onLongClickLabel,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onDoubleClick = onDoubleClick,
|
||||||
|
onClick = {
|
||||||
|
AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK)
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
fun Modifier.combinedClickableSound(
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
indication: Indication?,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClickLabel: String? = null,
|
||||||
|
role: Role? = null,
|
||||||
|
onLongClickLabel: String? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
onDoubleClick: (() -> Unit)? = null,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
): Modifier = this.combinedClickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = indication,
|
||||||
|
enabled = enabled,
|
||||||
|
onClickLabel = onClickLabel,
|
||||||
|
role = role,
|
||||||
|
onLongClickLabel = onLongClickLabel,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onDoubleClick = onDoubleClick,
|
||||||
|
onClick = {
|
||||||
|
AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK)
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UiText?.getString(): String? {
|
||||||
|
return this.parseString(LocalContext.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun isUsingDarkTheme(): Boolean {
|
||||||
|
if (LocalView.current.isInEditMode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val nightThemeMode = AppGlobal.preferences.getInt(
|
||||||
|
SettingsFragment.KEY_APPEARANCE_DARK_THEME,
|
||||||
|
SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME
|
||||||
|
)
|
||||||
|
val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||||
|
|
||||||
|
val systemUiNightMode = AppGlobal.resources.configuration.uiMode
|
||||||
|
|
||||||
|
val isSystemBatterySaver = AndroidUtils.isBatterySaverOn()
|
||||||
|
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 isUsingDynamicColors(): Boolean =
|
||||||
|
if (LocalView.current.isInEditMode) true
|
||||||
|
else {
|
||||||
|
AppGlobal.preferences.getBoolean(
|
||||||
|
SettingsFragment.KEY_USE_DYNAMIC_COLORS,
|
||||||
|
SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.meloda.fast.model.base.UiText
|
||||||
|
import com.meloda.fast.model.base.parseString
|
||||||
|
|
||||||
|
fun Context.showDialog(
|
||||||
|
title: UiText? = null,
|
||||||
|
message: UiText? = null,
|
||||||
|
isCancelable: Boolean = true,
|
||||||
|
positiveText: UiText? = null,
|
||||||
|
positiveAction: (() -> Unit)? = null,
|
||||||
|
negativeText: UiText? = null,
|
||||||
|
negativeAction: (() -> Unit)? = null,
|
||||||
|
neutralText: UiText? = null,
|
||||||
|
neutralAction: (() -> Unit)? = null,
|
||||||
|
onDismissAction: (() -> Unit)? = null,
|
||||||
|
view: View? = null,
|
||||||
|
items: List<UiText>? = null,
|
||||||
|
itemsChoiceType: ItemsChoiceType = ItemsChoiceType.None,
|
||||||
|
itemsClickAction: ((index: Int, value: String) -> Unit)? = null,
|
||||||
|
itemsMultiChoiceClickAction: ((index: Int, value: String, isChecked: Boolean) -> Unit)? = null,
|
||||||
|
checkedItems: List<Int>? = null
|
||||||
|
): AlertDialog {
|
||||||
|
val builder = MaterialAlertDialogBuilder(this)
|
||||||
|
.setCancelable(isCancelable)
|
||||||
|
.setOnDismissListener { onDismissAction?.invoke() }
|
||||||
|
|
||||||
|
title?.asString()?.let(builder::setTitle)
|
||||||
|
message?.asString()?.let(builder::setMessage)
|
||||||
|
|
||||||
|
view?.let(builder::setView)
|
||||||
|
|
||||||
|
positiveText?.let { text ->
|
||||||
|
builder.setPositiveButton(text.asString()) { _, _ -> positiveAction?.invoke() }
|
||||||
|
}
|
||||||
|
negativeText?.let { text ->
|
||||||
|
builder.setNegativeButton(text.asString()) { _, _ -> negativeAction?.invoke() }
|
||||||
|
}
|
||||||
|
neutralText?.let { text ->
|
||||||
|
builder.setNeutralButton(text.asString()) { _, _ -> neutralAction?.invoke() }
|
||||||
|
}
|
||||||
|
|
||||||
|
items?.mapNotNull { it.asString() }?.let { stringItems ->
|
||||||
|
when (itemsChoiceType) {
|
||||||
|
ItemsChoiceType.None -> {
|
||||||
|
builder.setItems(
|
||||||
|
stringItems.toTypedArray()
|
||||||
|
) { dialog, which ->
|
||||||
|
dialog.dismiss()
|
||||||
|
itemsClickAction?.invoke(which, stringItems[which])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemsChoiceType.SingleChoice -> {
|
||||||
|
builder.setSingleChoiceItems(
|
||||||
|
stringItems.toTypedArray(),
|
||||||
|
checkedItems?.first() ?: -1
|
||||||
|
) { _, which ->
|
||||||
|
itemsClickAction?.invoke(which, stringItems[which])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemsChoiceType.MultiChoice -> {
|
||||||
|
builder.setMultiChoiceItems(
|
||||||
|
stringItems.toTypedArray(),
|
||||||
|
BooleanArray(stringItems.size) { index -> checkedItems?.contains(index).isTrue }
|
||||||
|
) { _, which, isChecked ->
|
||||||
|
itemsMultiChoiceClickAction?.invoke(which, stringItems[which], isChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ItemsChoiceType {
|
||||||
|
object None : ItemsChoiceType()
|
||||||
|
object SingleChoice : ItemsChoiceType()
|
||||||
|
object MultiChoice : ItemsChoiceType()
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Context)
|
||||||
|
fun UiText?.asString(): String? {
|
||||||
|
return this.parseString(this@Context)
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.common.net.MediaType
|
||||||
|
import com.meloda.fast.common.AppGlobal
|
||||||
|
import com.meloda.fast.screens.settings.SettingsFragment
|
||||||
|
import com.meloda.fast.util.AndroidUtils
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@Deprecated("use resources or rewrite in Compose")
|
||||||
|
fun Int.dpToPx(): Int {
|
||||||
|
val metrics = Resources.getSystem().displayMetrics
|
||||||
|
return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("use resources or rewrite in Compose")
|
||||||
|
fun Float.dpToPx(): Int {
|
||||||
|
val metrics = Resources.getSystem().displayMetrics
|
||||||
|
return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val MediaType.mimeType: String get() = "${type()}/${subtype()}"
|
||||||
|
|
||||||
|
@Throws(NullPointerException::class)
|
||||||
|
fun <T> T?.notNull(lazyMessage: (() -> Any)? = null): T {
|
||||||
|
return if (lazyMessage != null) {
|
||||||
|
requireNotNull(this, lazyMessage)
|
||||||
|
} else {
|
||||||
|
requireNotNull(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> Iterable<T>.findIndex(predicate: (T) -> Boolean): Int? {
|
||||||
|
return indexOf(firstOrNull(predicate)).let { if (it == -1) null else it }
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T, K, M : MutableMap<in K, T>> Iterable<T>.toMap(
|
||||||
|
destination: M,
|
||||||
|
keySelector: (T) -> K,
|
||||||
|
): M {
|
||||||
|
for (element in this) {
|
||||||
|
val key = keySelector(element)
|
||||||
|
destination[key] = element
|
||||||
|
}
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isSystemUsingDarkMode(): Boolean {
|
||||||
|
val nightThemeMode = AppGlobal.preferences.getInt(
|
||||||
|
SettingsFragment.KEY_APPEARANCE_DARK_THEME,
|
||||||
|
SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME
|
||||||
|
)
|
||||||
|
val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||||
|
|
||||||
|
val systemUiNightMode = AppGlobal.resources.configuration.uiMode
|
||||||
|
|
||||||
|
val isSystemBatterySaver = AndroidUtils.isBatterySaverOn()
|
||||||
|
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 createTimerFlow(
|
||||||
|
time: Int,
|
||||||
|
onStartAction: suspend () -> Unit,
|
||||||
|
onTickAction: suspend (remainedTime: Int) -> Unit,
|
||||||
|
onTimeoutAction: suspend () -> Unit,
|
||||||
|
interval: Duration = 1.seconds
|
||||||
|
): Flow<Int> = (time downTo 0)
|
||||||
|
.asSequence()
|
||||||
|
.asFlow()
|
||||||
|
.onStart { onStartAction() }
|
||||||
|
.onEach { timeLeft ->
|
||||||
|
onTickAction(timeLeft)
|
||||||
|
if (timeLeft == 0) {
|
||||||
|
onTimeoutAction()
|
||||||
|
} 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(value, Dispatchers.Main)
|
||||||
|
|
||||||
|
context(ViewModel)
|
||||||
|
fun <T> MutableSharedFlow<T>.emitOnScope(
|
||||||
|
value: T,
|
||||||
|
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(dispatcher) {
|
||||||
|
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 }
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.meloda.fast.model.base.UiText
|
||||||
|
import com.meloda.fast.model.base.parseString
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
context(Fragment)
|
||||||
|
fun <T> Flow<T>.listenValue(
|
||||||
|
action: suspend (T) -> Unit
|
||||||
|
) = listenValue(lifecycleScope, action)
|
||||||
|
|
||||||
|
|
||||||
|
context(Fragment)
|
||||||
|
fun String.toast(duration: Int = Toast.LENGTH_LONG) = toast(requireContext(), duration)
|
||||||
|
|
||||||
|
context(Fragment)
|
||||||
|
fun color(@ColorRes resId: Int): Int {
|
||||||
|
return ContextCompat.getColor(requireContext(), resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Fragment)
|
||||||
|
fun drawable(@DrawableRes resId: Int): Drawable? {
|
||||||
|
return ContextCompat.getDrawable(requireContext(), resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Fragment)
|
||||||
|
fun string(@StringRes resId: Int): String {
|
||||||
|
return getString(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Fragment)
|
||||||
|
fun string(@StringRes resId: Int, vararg args: Any?): String {
|
||||||
|
return getString(resId, *args)
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Fragment)
|
||||||
|
fun UiText?.asString(): String? {
|
||||||
|
return this.parseString(this@Fragment.requireContext())
|
||||||
|
}
|
||||||
+74
-37
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.extensions
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
@@ -27,71 +27,85 @@ object ImageLoader {
|
|||||||
this.setImageDrawable(null)
|
this.setImageDrawable(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImageView.loadWithGlide(
|
fun ImageView.loadWithGlide(block: GlideParams.() -> Unit) {
|
||||||
url: String? = null,
|
val params = GlideParams()
|
||||||
uri: Uri? = null,
|
block.invoke(params)
|
||||||
drawableRes: Int? = null,
|
loadWithGlide(params)
|
||||||
drawable: Drawable? = null,
|
}
|
||||||
placeholderDrawable: Drawable? = null,
|
|
||||||
placeholderColor: Int? = null,
|
fun ImageView.loadWithGlide(params: GlideParams) {
|
||||||
errorDrawable: Drawable? = placeholderDrawable,
|
|
||||||
errorColor: Int? = null,
|
|
||||||
crossFade: Boolean = false,
|
|
||||||
crossFadeDuration: Int? = null,
|
|
||||||
asCircle: Boolean = false,
|
|
||||||
transformations: List<TypeTransformations> = emptyList(),
|
|
||||||
onLoadedAction: (() -> Unit)? = null,
|
|
||||||
onFailedAction: (() -> Unit)? = null,
|
|
||||||
priority: Priority = Priority.NORMAL,
|
|
||||||
cacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL
|
|
||||||
) {
|
|
||||||
val request = Glide.with(this)
|
val request = Glide.with(this)
|
||||||
|
|
||||||
var builder = when {
|
var builder = when {
|
||||||
url != null -> request.load(url)
|
params.imageUrl != null -> request.load(params.imageUrl)
|
||||||
uri != null -> request.load(uri)
|
params.imageUri != null -> request.load(params.imageUri)
|
||||||
drawableRes != null -> request.load(drawableRes)
|
params.drawableRes != null -> request.load(params.drawableRes)
|
||||||
drawable != null -> request.load(drawable)
|
drawable != null -> request.load(drawable)
|
||||||
else -> request.load(null as Drawable?)
|
else -> request.load(null as Drawable?)
|
||||||
}
|
}
|
||||||
|
|
||||||
val transforms = transformations.toMutableList()
|
val transforms = params.transformations.toMutableList()
|
||||||
if (asCircle) {
|
if (params.asCircle) {
|
||||||
transforms += TypeTransformations.CircleCrop
|
transforms += TypeTransformations.CircleCrop
|
||||||
}
|
}
|
||||||
|
|
||||||
builder = builder
|
builder = builder
|
||||||
.apply(TypeTransformations.createRequestOptions(transforms))
|
.apply(TypeTransformations.createRequestOptions(transforms))
|
||||||
.error(
|
.error(
|
||||||
errorDrawable
|
params.errorDrawable
|
||||||
?: if (errorColor != null) ColorDrawable(errorColor) else null
|
?: if (params.errorColor != null) {
|
||||||
|
ColorDrawable(requireNotNull(params.errorColor))
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
.placeholder(
|
.placeholder(
|
||||||
placeholderDrawable
|
params.placeholderDrawable
|
||||||
?: if (placeholderColor != null) ColorDrawable(placeholderColor) else null
|
?: if (params.placeholderColor != null) {
|
||||||
|
ColorDrawable(requireNotNull(params.placeholderColor))
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
.addListener(ImageLoadRequestListener(onLoadedAction, onFailedAction))
|
.addListener(ImageLoadRequestListener(params.onLoadedAction, params.onFailedAction))
|
||||||
.diskCacheStrategy(cacheStrategy)
|
.addListener(ImageLoadDoneListener(params.onDoneAction))
|
||||||
.priority(priority)
|
.diskCacheStrategy(params.cacheStrategy)
|
||||||
|
.priority(params.loadPriority)
|
||||||
|
|
||||||
if (crossFade || crossFadeDuration != null) {
|
if (params.crossFade || params.crossFadeDuration != null) {
|
||||||
builder = builder.transition(withCrossFade(crossFadeDuration ?: 200))
|
builder = builder.transition(withCrossFade(params.crossFadeDuration ?: 200))
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.into(this)
|
builder.into(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GlideParams(
|
||||||
|
var imageUrl: String? = null,
|
||||||
|
var imageUri: Uri? = null,
|
||||||
|
var drawableRes: Int? = null,
|
||||||
|
var imageDrawable: Drawable? = null,
|
||||||
|
var placeholderDrawable: Drawable? = null,
|
||||||
|
var placeholderColor: Int? = null,
|
||||||
|
var errorDrawable: Drawable? = placeholderDrawable,
|
||||||
|
var errorColor: Int? = null,
|
||||||
|
var crossFade: Boolean = false,
|
||||||
|
var crossFadeDuration: Int? = null,
|
||||||
|
var asCircle: Boolean = false,
|
||||||
|
var transformations: List<TypeTransformations> = emptyList(),
|
||||||
|
var onLoadedAction: (() -> Unit)? = null,
|
||||||
|
var onFailedAction: (() -> Unit)? = null,
|
||||||
|
var onDoneAction: (() -> Unit)? = null,
|
||||||
|
var loadPriority: Priority = Priority.NORMAL,
|
||||||
|
var cacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL,
|
||||||
|
)
|
||||||
|
|
||||||
class ImageLoadRequestListener(
|
class ImageLoadRequestListener(
|
||||||
private val onLoadedAction: (() -> Unit)?,
|
private val onLoadedAction: (() -> Unit)?,
|
||||||
private val onFailedAction: (() -> Unit)?
|
private val onFailedAction: (() -> Unit)?,
|
||||||
) : RequestListener<Drawable> {
|
) : RequestListener<Drawable> {
|
||||||
|
|
||||||
override fun onLoadFailed(
|
override fun onLoadFailed(
|
||||||
e: GlideException?,
|
e: GlideException?,
|
||||||
model: Any?,
|
model: Any?,
|
||||||
target: Target<Drawable>?,
|
target: Target<Drawable>?,
|
||||||
isFirstResource: Boolean
|
isFirstResource: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
onFailedAction?.invoke()
|
onFailedAction?.invoke()
|
||||||
return false
|
return false
|
||||||
@@ -102,13 +116,36 @@ class ImageLoadRequestListener(
|
|||||||
model: Any?,
|
model: Any?,
|
||||||
target: Target<Drawable>?,
|
target: Target<Drawable>?,
|
||||||
dataSource: DataSource?,
|
dataSource: DataSource?,
|
||||||
isFirstResource: Boolean
|
isFirstResource: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
onLoadedAction?.invoke()
|
onLoadedAction?.invoke()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ImageLoadDoneListener(private val onDoneAction: (() -> Unit)?) : RequestListener<Drawable> {
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Drawable>?,
|
||||||
|
isFirstResource: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
onDoneAction?.invoke()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: Drawable?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Drawable>?,
|
||||||
|
dataSource: DataSource?,
|
||||||
|
isFirstResource: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
onDoneAction?.invoke()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sealed class TypeTransformations {
|
sealed class TypeTransformations {
|
||||||
|
|
||||||
object CenterCrop : TypeTransformations()
|
object CenterCrop : TypeTransformations()
|
||||||
@@ -123,7 +160,7 @@ sealed class TypeTransformations {
|
|||||||
val topLeft: Float,
|
val topLeft: Float,
|
||||||
val topRight: Float,
|
val topRight: Float,
|
||||||
val bottomRight: Float,
|
val bottomRight: Float,
|
||||||
val bottomLeft: Float
|
val bottomLeft: Float,
|
||||||
) : TypeTransformations()
|
) : TypeTransformations()
|
||||||
|
|
||||||
fun toGlideTransform(): Transformation<Bitmap> = when (this) {
|
fun toGlideTransform(): Transformation<Bitmap> = when (this) {
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.meloda.fast.model.base.UiText
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toast(context: Context, duration: Int = Toast.LENGTH_LONG) {
|
||||||
|
Toast.makeText(context, this, duration).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
context (Context)
|
||||||
|
fun String.toast(duration: Int = Toast.LENGTH_LONG) = toast(this@Context, duration)
|
||||||
|
|
||||||
|
fun String.asUiText(): UiText = UiText.Simple(this)
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.meloda.fast.ext
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.view.*
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.meloda.fast.R
|
||||||
|
import com.meloda.fast.common.AppGlobal
|
||||||
|
import com.meloda.fast.databinding.ToolbarMenuItemAvatarBinding
|
||||||
|
import com.meloda.fast.ext.ImageLoader.loadWithGlide
|
||||||
|
|
||||||
|
val EditText.trimmedText: String get() = text.toString().trim()
|
||||||
|
fun EditText.selectLast() {
|
||||||
|
setSelection(text.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun EditText.onDone(crossinline callback: () -> Unit) {
|
||||||
|
imeOptions = EditorInfo.IME_ACTION_DONE
|
||||||
|
maxLines = 1
|
||||||
|
setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
callback.invoke()
|
||||||
|
return@setOnEditorActionListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("use InsetManager")
|
||||||
|
fun View.showKeyboard(flags: Int = 0) {
|
||||||
|
(AppGlobal.Instance.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||||
|
.showSoftInput(this, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("use InsetManager")
|
||||||
|
fun View.hideKeyboard(focusedView: View? = null, flags: Int = 0) {
|
||||||
|
(AppGlobal.Instance.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||||
|
.hideSoftInputFromWindow(focusedView?.windowToken ?: this.windowToken, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextInputLayout.clearError() {
|
||||||
|
if (error != null) error = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextInputLayout.toggleError(errorText: String, isNeedToShow: Boolean) {
|
||||||
|
if (isNeedToShow) {
|
||||||
|
this.error = errorText
|
||||||
|
} else {
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextInputLayout.clearTextOnErrorIconClick(textField: TextInputEditText) {
|
||||||
|
setErrorIconOnClickListener {
|
||||||
|
textField.text = null
|
||||||
|
textField.showKeyboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun ImageView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
|
||||||
|
visibility = if (drawable != null) View.VISIBLE else visibilityWhenFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
|
||||||
|
visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setMarginsPx(
|
||||||
|
@Px leftMargin: Int? = null,
|
||||||
|
@Px topMargin: Int? = null,
|
||||||
|
@Px rightMargin: Int? = null,
|
||||||
|
@Px bottomMargin: Int? = null,
|
||||||
|
) {
|
||||||
|
(layoutParams as? ViewGroup.MarginLayoutParams)?.let { params ->
|
||||||
|
leftMargin?.run { params.leftMargin = this }
|
||||||
|
topMargin?.run { params.topMargin = this }
|
||||||
|
rightMargin?.run { params.rightMargin = this }
|
||||||
|
bottomMargin?.run { params.bottomMargin = this }
|
||||||
|
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextView.clear() {
|
||||||
|
text = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.invisible() = run { isInvisible = true }
|
||||||
|
fun View.visible() = run { isVisible = true }
|
||||||
|
fun View.gone() = run { isGone = true }
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun View.toggleVisibility(visible: Boolean?, visibilityWhenFalse: Int = View.GONE) =
|
||||||
|
run { visibility = if (visible == true) View.VISIBLE else visibilityWhenFalse }
|
||||||
|
|
||||||
|
fun Toolbar.tintMenuItemIcons(@ColorInt colorToTint: Int) {
|
||||||
|
menu.forEach { item ->
|
||||||
|
item.icon?.setTint(colorToTint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Toolbar.addAvatarMenuItem(urlToLoad: String? = null, drawable: Drawable? = null): MenuItem {
|
||||||
|
val avatarMenuItemBinding = ToolbarMenuItemAvatarBinding.inflate(
|
||||||
|
LayoutInflater.from(context), null, false
|
||||||
|
)
|
||||||
|
|
||||||
|
val avatarMenuItem = menu.add(context.getString(R.string.navigation_profile))
|
||||||
|
avatarMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
||||||
|
avatarMenuItem.actionView = avatarMenuItemBinding.root
|
||||||
|
|
||||||
|
val imageView = avatarMenuItemBinding.avatar
|
||||||
|
|
||||||
|
when {
|
||||||
|
urlToLoad != null -> {
|
||||||
|
imageView.loadWithGlide {
|
||||||
|
imageUrl = urlToLoad
|
||||||
|
transformations = ImageLoader.userAvatarTransformations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable != null -> {
|
||||||
|
imageView.loadWithGlide {
|
||||||
|
imageDrawable = drawable
|
||||||
|
transformations = ImageLoader.userAvatarTransformations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatarMenuItem
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.doOnApplyWindowInsets(
|
||||||
|
block: (
|
||||||
|
view: View,
|
||||||
|
insets: WindowInsetsCompat,
|
||||||
|
paddings: Rect,
|
||||||
|
margins: Rect
|
||||||
|
) -> WindowInsetsCompat
|
||||||
|
) {
|
||||||
|
val initialPaddings = recordInitialPaddingsForView(this)
|
||||||
|
val initialMargins = recordInitialMarginsForView(this)
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
||||||
|
block(view, insets, initialPaddings, initialMargins)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestApplyInsetsWhenAttached()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recordInitialPaddingsForView(view: View) =
|
||||||
|
Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||||
|
|
||||||
|
private fun recordInitialMarginsForView(view: View) =
|
||||||
|
Rect(view.marginStart, view.marginTop, view.marginEnd, view.marginBottom)
|
||||||
|
|
||||||
|
fun View.requestApplyInsetsWhenAttached() {
|
||||||
|
if (isAttachedToWindow) {
|
||||||
|
requestApplyInsets()
|
||||||
|
} else {
|
||||||
|
doOnAttach { requestApplyInsets() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EditText.updateTextIfDiffer(text: String?) {
|
||||||
|
if (this.text?.toString() == text) return
|
||||||
|
setText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.bulkIsEnabled(isEnabled: Boolean) {
|
||||||
|
this.isEnabled = isEnabled
|
||||||
|
toggleChildrenIsEnabled(isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.toggleChildrenIsEnabled(isEnabled: Boolean) {
|
||||||
|
children.forEach { view -> view.toggleIsEnabled(isEnabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.toggleIsEnabled(isEnabled: Boolean) {
|
||||||
|
this.isEnabled = isEnabled
|
||||||
|
}
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
package com.meloda.fast.extensions
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.util.SparseArray
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.annotation.Px
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.core.view.forEach
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.google.common.net.MediaType
|
|
||||||
import com.meloda.fast.common.AppGlobal
|
|
||||||
import com.meloda.fast.databinding.ToolbarMenuItemAvatarBinding
|
|
||||||
import com.meloda.fast.extensions.ImageLoader.loadWithGlide
|
|
||||||
|
|
||||||
fun Int.dpToPx(): Int {
|
|
||||||
val metrics = Resources.getSystem().displayMetrics
|
|
||||||
return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Float.dpToPx(): Int {
|
|
||||||
val metrics = Resources.getSystem().displayMetrics
|
|
||||||
return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TextView.clear() {
|
|
||||||
text = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ViewGroup.saveChildViewStates(): SparseArray<Parcelable> {
|
|
||||||
val childViewStates = SparseArray<Parcelable>()
|
|
||||||
children.forEach { child -> child.saveHierarchyState(childViewStates) }
|
|
||||||
return childViewStates
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ViewGroup.restoreChildViewStates(childViewStates: SparseArray<Parcelable>) {
|
|
||||||
children.forEach { child -> child.restoreHierarchyState(childViewStates) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.invisible() = run { visibility = View.INVISIBLE }
|
|
||||||
|
|
||||||
fun View.visible() = run { visibility = View.VISIBLE }
|
|
||||||
fun View.gone() = run { visibility = View.GONE }
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun View.toggleVisibility(visible: Boolean?, visibilityWhenFalse: Int = View.GONE) =
|
|
||||||
run { visibility = if (visible == true) View.VISIBLE else visibilityWhenFalse }
|
|
||||||
|
|
||||||
fun ValueAnimator.startWithIntValues(from: Int, to: Int) {
|
|
||||||
setIntValues(from, to)
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ValueAnimator.startWithFloatValues(from: Float, to: Float) {
|
|
||||||
setFloatValues(from, to)
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.setMarginsPx(
|
|
||||||
@Px leftMargin: Int? = null,
|
|
||||||
@Px topMargin: Int? = null,
|
|
||||||
@Px rightMargin: Int? = null,
|
|
||||||
@Px bottomMargin: Int? = null
|
|
||||||
) {
|
|
||||||
if (layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
val params = layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
leftMargin?.run { params.leftMargin = this }
|
|
||||||
topMargin?.run { params.topMargin = this }
|
|
||||||
rightMargin?.run { params.rightMargin = this }
|
|
||||||
bottomMargin?.run { params.bottomMargin = this }
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T, K> Pair<T?, K?>.runIfElementsNotNull(block: (T, K) -> Unit) {
|
|
||||||
val firstCopy = first
|
|
||||||
val secondCopy = second
|
|
||||||
if (firstCopy != null && secondCopy != null) {
|
|
||||||
block(firstCopy, secondCopy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun ImageView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
|
|
||||||
visibility = if (drawable != null) View.VISIBLE else visibilityWhenFalse
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
|
|
||||||
visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.showKeyboard(flags: Int = 0) {
|
|
||||||
AppGlobal.inputMethodManager.showSoftInput(this, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.hideKeyboard(focusedView: View? = null, flags: Int = 0) {
|
|
||||||
AppGlobal.inputMethodManager.hideSoftInputFromWindow(
|
|
||||||
focusedView?.windowToken ?: this.windowToken, flags
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Toolbar.tintMenuItemIcons(@ColorInt colorToTint: Int) {
|
|
||||||
menu.forEach { item ->
|
|
||||||
item.icon?.setTint(colorToTint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Toolbar.addAvatarMenuItem(urlToLoad: String? = null, drawable: Drawable? = null): MenuItem {
|
|
||||||
val avatarMenuItemBinding = ToolbarMenuItemAvatarBinding.inflate(
|
|
||||||
LayoutInflater.from(context), null, false
|
|
||||||
)
|
|
||||||
|
|
||||||
val avatarMenuItem = menu.add("Profile")
|
|
||||||
avatarMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
|
||||||
avatarMenuItem.actionView = avatarMenuItemBinding.root
|
|
||||||
|
|
||||||
val imageView = avatarMenuItemBinding.avatar
|
|
||||||
|
|
||||||
when {
|
|
||||||
urlToLoad != null -> {
|
|
||||||
imageView.loadWithGlide(
|
|
||||||
url = urlToLoad,
|
|
||||||
transformations = ImageLoader.userAvatarTransformations
|
|
||||||
)
|
|
||||||
}
|
|
||||||
drawable != null -> {
|
|
||||||
imageView.loadWithGlide(
|
|
||||||
drawable = drawable,
|
|
||||||
transformations = ImageLoader.userAvatarTransformations
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return avatarMenuItem
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<T>.notifyObservers() {
|
|
||||||
this.value = this.value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<T>.setIfNotEquals(item: T) {
|
|
||||||
if (this.value != item) this.value = item
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<T>.requireValue(): T {
|
|
||||||
return this.value!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val EditText.trimmedText: String get() = text.toString().trim()
|
|
||||||
|
|
||||||
val MediaType.mimeType: String get() = "${type()}/${subtype()}"
|
|
||||||
|
|
||||||
fun EditText.selectLast() {
|
|
||||||
setSelection(text.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> T?.requireNotNull(): T {
|
|
||||||
return requireNotNull(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun String?.orDots(count: Int = 3): String {
|
|
||||||
return this ?: ("." * count)
|
|
||||||
}
|
|
||||||
|
|
||||||
private operator fun String.times(count: Int): String {
|
|
||||||
val builder = StringBuilder()
|
|
||||||
for (i in 0 until count) {
|
|
||||||
builder.append(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.model
|
|
||||||
|
|
||||||
abstract class DataItem<IdType> {
|
|
||||||
abstract val dataItemId: IdType
|
|
||||||
|
|
||||||
object Header : DataItem<Int>() {
|
|
||||||
override val dataItemId = Int.MIN_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
object Footer : DataItem<Int>() {
|
|
||||||
override val dataItemId = Int.MIN_VALUE + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,10 @@ import kotlinx.parcelize.IgnoredOnParcel
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class SelectableItem constructor(
|
open class SelectableItem : Parcelable {
|
||||||
@Ignore
|
|
||||||
val selectableItemId: Int = 0
|
|
||||||
) : DataItem<Int>(), Parcelable {
|
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
var isSelected: Boolean = false
|
var isSelected: Boolean = false
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val dataItemId = selectableItemId
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ data class UpdateItem(
|
|||||||
val originalName: String,
|
val originalName: String,
|
||||||
val fileSize: Int,
|
val fileSize: Int,
|
||||||
val preRelease: Int,
|
val preRelease: Int,
|
||||||
val downloadLink: String
|
val downloadLink: String,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun isMandatory(): Boolean = mandatory == 1
|
fun isMandatory(): Boolean = mandatory == 1
|
||||||
@@ -29,6 +29,24 @@ data class UpdateItem(
|
|||||||
return Gson().toJson(this)
|
return Gson().toJson(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EMPTY = UpdateItem(
|
||||||
|
id = 0,
|
||||||
|
versionName = "1.0.0",
|
||||||
|
versionCode = 2,
|
||||||
|
mandatory = 1,
|
||||||
|
changelog = "Some kind of simple changelog",
|
||||||
|
enabled = 1,
|
||||||
|
fileName = "bruhmeme.apk",
|
||||||
|
date = System.currentTimeMillis(),
|
||||||
|
extension = "",
|
||||||
|
originalName = "",
|
||||||
|
fileSize = 0,
|
||||||
|
preRelease = 0,
|
||||||
|
downloadLink = "https://c4.kemono.party/data/98/8c/988cf166f1ee9cd318e2407e6bfbabf60bffa53ed229ea0b2434009f1598e039.png?f=JessieGym002b4pt.png"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.meloda.fast.model.base
|
||||||
|
|
||||||
|
interface AdapterDiffItem {
|
||||||
|
|
||||||
|
val id: Int
|
||||||
|
|
||||||
|
fun areItemsTheSame(newItem: AdapterDiffItem): Boolean {
|
||||||
|
return id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun areContentsTheSame(newItem: AdapterDiffItem): Boolean {
|
||||||
|
return this == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.meloda.fast.model.base
|
||||||
|
|
||||||
|
interface DisplayableItem
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.meloda.fast.model.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.meloda.fast.ext.GlideParams
|
||||||
|
import com.meloda.fast.ext.ImageLoader.loadWithGlide
|
||||||
|
|
||||||
|
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 getResourceId(): Int? = when(this) {
|
||||||
|
is Resource -> this.resId
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ImageView.setImage(image: UiImage, glideBlock: GlideParams.() -> Unit) {
|
||||||
|
val glideParams = GlideParams()
|
||||||
|
glideBlock.invoke(glideParams)
|
||||||
|
this.setImage(image, glideParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ImageView.setImage(image: UiImage, glideParams: GlideParams? = null) {
|
||||||
|
image.attachTo(this, glideParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UiImage?.attachTo(imageView: ImageView, glideBlock: GlideParams.() -> Unit) {
|
||||||
|
val glideParams = GlideParams()
|
||||||
|
glideBlock.invoke(glideParams)
|
||||||
|
this.attachTo(imageView, glideParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UiImage?.attachTo(imageView: ImageView, glideParams: GlideParams? = null) {
|
||||||
|
when (this) {
|
||||||
|
is UiImage.Simple -> imageView.setImageDrawable(drawable)
|
||||||
|
is UiImage.Resource -> imageView.setImageResource(resId)
|
||||||
|
is UiImage.Color -> imageView.setImageDrawable(ColorDrawable(color))
|
||||||
|
is UiImage.ColorResource -> imageView.setImageDrawable(
|
||||||
|
ColorDrawable(ContextCompat.getColor(imageView.context, resId))
|
||||||
|
)
|
||||||
|
|
||||||
|
is UiImage.Url -> glideParams?.let { params ->
|
||||||
|
params.imageUrl = url
|
||||||
|
imageView.loadWithGlide(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UiImage?.asDrawable(context: Context): Drawable? {
|
||||||
|
return when (this) {
|
||||||
|
is UiImage.Simple -> drawable
|
||||||
|
is UiImage.Resource -> ContextCompat.getDrawable(context, resId)
|
||||||
|
is UiImage.Color -> ColorDrawable(color)
|
||||||
|
is UiImage.ColorResource -> ColorDrawable(ContextCompat.getColor(context, resId))
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UiImage?.getImage(): Any? {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return when(this) {
|
||||||
|
is UiImage.Color -> ColorDrawable(color)
|
||||||
|
is UiImage.ColorResource -> ColorDrawable(ContextCompat.getColor(context, resId))
|
||||||
|
is UiImage.Resource -> ContextCompat.getDrawable(context, resId)
|
||||||
|
is UiImage.Simple -> drawable
|
||||||
|
is UiImage.Url -> url
|
||||||
|
null -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.meloda.fast.model.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.PluralsRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.RawValue
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
sealed class UiText : Parcelable {
|
||||||
|
|
||||||
|
data class Resource(@StringRes val resId: Int) : UiText()
|
||||||
|
|
||||||
|
data class ResourceParams(
|
||||||
|
@StringRes val value: Int,
|
||||||
|
val args: List<@RawValue Any?>,
|
||||||
|
) : UiText()
|
||||||
|
|
||||||
|
data class Simple(val text: String) : UiText()
|
||||||
|
|
||||||
|
data class QuantityResource(@PluralsRes val resId: Int, val quantity: Int) : UiText()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UiText?.parseString(context: Context): String? {
|
||||||
|
return when (this) {
|
||||||
|
is UiText.Resource -> context.getString(resId)
|
||||||
|
is UiText.ResourceParams -> {
|
||||||
|
val processedArgs = args.map { any ->
|
||||||
|
when (any) {
|
||||||
|
is UiText -> any.parseString(context)
|
||||||
|
else -> any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.getString(value, *processedArgs.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
is UiText.QuantityResource -> context.resources.getQuantityString(resId, quantity, quantity)
|
||||||
|
is UiText.Simple -> text
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user