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.cxx.configure.gradleLocalProperties
|
||||
|
||||
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage", "\"\"")
|
||||
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint", "\"\"")
|
||||
@@ -17,13 +19,14 @@ plugins {
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
id("kotlin-parcelize")
|
||||
id("dagger.hilt.android.plugin")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.fast"
|
||||
|
||||
compileSdk = 32
|
||||
compileSdk = 34
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.all {
|
||||
@@ -34,14 +37,14 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.meloda.fast"
|
||||
minSdk = 23
|
||||
targetSdk = 32
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "alpha"
|
||||
|
||||
javaCompileOptions {
|
||||
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 {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjvm-default=compatibility", "-opt-in=kotlin.RequiresOptIn")
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.5"
|
||||
useLiveLiterals = true
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
|
||||
@@ -92,74 +128,99 @@ fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
|
||||
val currentTime get() = (System.currentTimeMillis() / 1000).toInt()
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("reflect", "1.6.10"))
|
||||
|
||||
implementation(libs.androidx.core)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
implementation(libs.androidx.lifecycle.livedata)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
|
||||
implementation(libs.androidx.lifecycle.common.java8)
|
||||
// DI zone
|
||||
implementation("io.insert-koin:koin-android:3.4.0")
|
||||
// end of DI zone
|
||||
|
||||
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(libs.androidx.room.runtime)
|
||||
kapt(libs.androidx.room.compiler)
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
|
||||
implementation(libs.cicerone)
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
implementation(libs.waveformSeekBar)
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||
|
||||
implementation(libs.glide)
|
||||
kapt(libs.glide.compiler)
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
|
||||
implementation(libs.kPermissions)
|
||||
implementation(libs.kPermissions.coroutines)
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
|
||||
|
||||
implementation(libs.appCenter.analytics)
|
||||
implementation(libs.appCenter.crashes)
|
||||
implementation("androidx.room:room-ktx:2.5.2")
|
||||
implementation("androidx.room:room-runtime:2.5.2")
|
||||
ksp("androidx.room:room-compiler:2.5.2")
|
||||
|
||||
implementation(libs.hilt)
|
||||
kapt(libs.hilt.compiler)
|
||||
implementation("com.github.terrakok:cicerone:7.1")
|
||||
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.gson.converter)
|
||||
implementation("com.github.massoudss:waveformSeekBar:5.0.0")
|
||||
|
||||
implementation(libs.okhttp3)
|
||||
implementation(libs.okhttp3.interceptor)
|
||||
implementation("com.github.bumptech.glide:glide:4.15.1")
|
||||
ksp("com.github.bumptech.glide:compiler:4.15.1")
|
||||
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.coroutines.android)
|
||||
implementation("com.github.fondesa:kpermissions:3.4.0")
|
||||
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">
|
||||
|
||||
<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.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.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".common.AppGlobal"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:supportsRtl="false"
|
||||
android:testOnly="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="DataExtractionRules"
|
||||
tools:replace="android:allowBackup"
|
||||
tools:ignore="DataExtractionRules">
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<activity
|
||||
android:name=".screens.main.MainActivity"
|
||||
android:theme="@style/AppTheme.Splash"
|
||||
android:name=".screens.main.activity.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -40,15 +39,64 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".screens.testing.TestActivity" />
|
||||
|
||||
<service
|
||||
android:name=".service.LongPollService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".service.OnlineService"
|
||||
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
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@@ -60,5 +108,4 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -7,8 +7,6 @@ enum class ApiEvent(val value: Int) {
|
||||
MessageEdit(5),
|
||||
MessageReadIncoming(6),
|
||||
MessageReadOutgoing(7),
|
||||
FriendOnline(8),
|
||||
FriendOffline(9),
|
||||
MessagesDeleted(13),
|
||||
PinUnpinConversation(20),
|
||||
PrivateTyping(61),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.model.AppAccount
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
object UserConfig {
|
||||
|
||||
@@ -42,6 +42,6 @@ object UserConfig {
|
||||
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 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 API_VERSION = "5.189"
|
||||
const val API_VERSION = "5.173"
|
||||
const val LP_VERSION = 10
|
||||
|
||||
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,
|
||||
@SerializedName("error_msg", alternate = ["error_description"])
|
||||
open val errorMessage: String? = null,
|
||||
@SerializedName("error_type")
|
||||
val errorType: String? = null,
|
||||
val throwable: Throwable? = null
|
||||
) : 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(
|
||||
val message: VkMessage,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
val groups: HashMap<Int, VkGroup>,
|
||||
) : LongPollEvent()
|
||||
|
||||
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent()
|
||||
|
||||
data class VkMessageReadIncomingEvent(val peerId: Int, val messageId: Int) : LongPollEvent()
|
||||
data class VkMessageReadOutgoingEvent(val peerId: Int, val messageId: Int) : LongPollEvent()
|
||||
data class VkMessageReadIncomingEvent(
|
||||
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.base.viewmodel.VkEventCallback
|
||||
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.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
@@ -47,10 +52,8 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
|
||||
ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
|
||||
ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
|
||||
ApiEvent.FriendOnline -> parseFriendOnline(eventType, event)
|
||||
ApiEvent.FriendOffline -> parseFriendOffline(eventType, event)
|
||||
ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
|
||||
ApiEvent.PinUnpinConversation -> onNewEvent(eventType, event)
|
||||
ApiEvent.PinUnpinConversation -> parseConversationPinStateChanged(eventType, event)
|
||||
ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
|
||||
ApiEvent.ChatTyping -> onNewEvent(eventType, event)
|
||||
ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
|
||||
@@ -67,6 +70,27 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
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) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
@@ -119,6 +143,7 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val peerId = event[1].asInt
|
||||
val messageId = event[2].asInt
|
||||
val unreadCount = event[3].asInt
|
||||
|
||||
launch {
|
||||
listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
|
||||
@@ -127,7 +152,8 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadIncomingEvent(
|
||||
peerId = peerId,
|
||||
messageId = messageId
|
||||
messageId = messageId,
|
||||
unreadCount = unreadCount
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -139,6 +165,7 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val peerId = event[1].asInt
|
||||
val messageId = event[2].asInt
|
||||
val unreadCount = event[3].asInt
|
||||
|
||||
launch {
|
||||
listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
|
||||
@@ -147,7 +174,8 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadOutgoingEvent(
|
||||
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) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
|
||||
coroutineScope {
|
||||
suspendCoroutine<T> {
|
||||
suspendCoroutine {
|
||||
launch {
|
||||
val normalMessageResponse = messagesRepository.getById(
|
||||
MessagesGetByIdRequest(
|
||||
@@ -179,7 +199,7 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
)
|
||||
)
|
||||
|
||||
if (!normalMessageResponse.isSuccessful()) {
|
||||
if (normalMessageResponse.isError()) {
|
||||
normalMessageResponse.error.throwable?.run { throw this }
|
||||
}
|
||||
|
||||
@@ -195,12 +215,12 @@ class LongPollUpdatesParser(private val messagesRepository: MessagesRepository)
|
||||
|
||||
val profiles = hashMapOf<Int, VkUser>()
|
||||
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>()
|
||||
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) {
|
||||
@@ -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>) {
|
||||
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> {
|
||||
return object : VkEventCallback<R> {
|
||||
override fun onEvent(event: R) = block.invoke(event)
|
||||
}
|
||||
internal inline fun <R : Any> assembleEventCallback(
|
||||
crossinline block: (R) -> Unit,
|
||||
): 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.model.attachments.VkAttachment
|
||||
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.util.TimeUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
// TODO: 05.08.2023, Danil Nikolaev: create other class for storing in database
|
||||
@Entity(tableName = "messages")
|
||||
@Parcelize
|
||||
data class VkMessage constructor(
|
||||
@@ -38,7 +40,7 @@ data class VkMessage constructor(
|
||||
var replyMessage: VkMessage? = null,
|
||||
|
||||
val geo: BaseVkMessage.Geo? = null,
|
||||
) : SelectableItem(id) {
|
||||
) : SelectableItem() {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
@@ -48,6 +50,14 @@ data class VkMessage constructor(
|
||||
@IgnoredOnParcel
|
||||
var group: VkGroup? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var actionUser: VkUser? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var actionGroup: VkGroup? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var state: State = State.Sent
|
||||
@@ -58,7 +68,7 @@ data class VkMessage constructor(
|
||||
|
||||
fun isGroup() = fromId < 0
|
||||
|
||||
fun isRead(conversation: VkConversation) =
|
||||
fun isRead(conversation: VkConversationDomain) =
|
||||
if (isOut) {
|
||||
conversation.outRead - id >= 0
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,8 @@ data class VkUser(
|
||||
val online: Boolean,
|
||||
val photo200: String?,
|
||||
val lastSeen: Int?,
|
||||
val lastSeenStatus: String?
|
||||
val lastSeenStatus: String?,
|
||||
val birthday: String?
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString() = fullName
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.model.DataItem
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
open class VkAttachment : DataItem<Int>(), Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val dataItemId: Int = -1
|
||||
open class VkAttachment : Parcelable {
|
||||
|
||||
open fun asString(withAccessKey: Boolean = true) = ""
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkCurator(
|
||||
val id: Int
|
||||
) : VkAttachment()
|
||||
val id: Int,
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -14,4 +15,7 @@ data class VkStory(
|
||||
|
||||
fun isFromGroup() = ownerId < 0
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ data class VkVideo(
|
||||
val images: List<VideoImage>,
|
||||
val firstFrames: List<BaseVkVideo.FirstFrame>?,
|
||||
val accessKey: String?,
|
||||
val title: String
|
||||
val title: String,
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
@@ -47,11 +47,11 @@ data class VkVideo(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val withPadding: Boolean
|
||||
val withPadding: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
var shapeKind: ShapeKind
|
||||
var shapeKind: ShapeKind? = null
|
||||
|
||||
init {
|
||||
val ratio = width.toFloat() / height.toFloat()
|
||||
@@ -64,10 +64,21 @@ data class VkVideo(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ShapeKind {
|
||||
object Vertical : ShapeKind()
|
||||
object Horizontal : ShapeKind()
|
||||
object Square : ShapeKind()
|
||||
open class ShapeKind(val value: Int) {
|
||||
object Square : ShapeKind(0)
|
||||
object Vertical : ShapeKind(1)
|
||||
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(
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkWidget(
|
||||
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?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkGroup() = VkGroup(
|
||||
fun mapToDomain() = VkGroup(
|
||||
id = -id,
|
||||
name = name,
|
||||
screenName = screen_name,
|
||||
|
||||
@@ -18,7 +18,8 @@ data class BaseVkUser(
|
||||
val photo_200: String?,
|
||||
val online: Int?,
|
||||
val online_info: OnlineInfo?,
|
||||
val screen_name: String
|
||||
val screen_name: String,
|
||||
val bdate: String?
|
||||
//...other fields
|
||||
) : Parcelable {
|
||||
|
||||
@@ -32,14 +33,15 @@ data class BaseVkUser(
|
||||
val app_id: Int?
|
||||
) : Parcelable
|
||||
|
||||
fun asVkUser() = VkUser(
|
||||
fun mapToDomain() = VkUser(
|
||||
id = id,
|
||||
firstName = first_name,
|
||||
lastName = last_name,
|
||||
online = online == 1,
|
||||
photo200 = photo_200,
|
||||
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 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.VkUser
|
||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
|
||||
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -12,6 +15,8 @@ data class BaseVkConversation(
|
||||
val last_message_id: Int,
|
||||
val in_read: Int,
|
||||
val out_read: Int,
|
||||
val in_read_cmid: Int,
|
||||
val out_read_cmid: Int,
|
||||
val sort_id: SortId,
|
||||
val last_conversation_message_id: Int,
|
||||
val is_marked_unread: Boolean,
|
||||
@@ -22,43 +27,20 @@ data class BaseVkConversation(
|
||||
val can_receive_money: Boolean,
|
||||
val chat_settings: ChatSettings?,
|
||||
val call_in_progress: CallInProgress?,
|
||||
val unread_count: Int?
|
||||
val unread_count: Int?,
|
||||
) : 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
|
||||
data class Peer(
|
||||
val id: Int,
|
||||
val type: String,
|
||||
val local_id: Int
|
||||
val local_id: Int,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class SortId(
|
||||
val major_id: Int,
|
||||
val minor_id: Int
|
||||
val minor_id: Int,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@@ -66,12 +48,12 @@ data class BaseVkConversation(
|
||||
val disabled_forever: Boolean,
|
||||
val no_sound: Boolean,
|
||||
val disabled_mentions: Boolean,
|
||||
val disabled_mass_mentions: Boolean
|
||||
val disabled_mass_mentions: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class CanWrite(
|
||||
val allowed: Boolean
|
||||
val allowed: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@@ -89,7 +71,7 @@ data class BaseVkConversation(
|
||||
val is_disappearing: Boolean,
|
||||
val is_service: Boolean,
|
||||
val theme: String?,
|
||||
val pinned_message: BaseVkMessage?
|
||||
val pinned_message: BaseVkMessage?,
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
@@ -104,7 +86,7 @@ data class BaseVkConversation(
|
||||
val can_copy_chat: Boolean,
|
||||
val can_call: Boolean,
|
||||
val can_use_mass_mentions: Boolean,
|
||||
val can_change_style: Boolean
|
||||
val can_change_style: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@@ -112,21 +94,54 @@ data class BaseVkConversation(
|
||||
val photo_50: String?,
|
||||
val photo_100: String?,
|
||||
val photo_200: String?,
|
||||
val is_default_photo: Boolean
|
||||
val is_default_photo: Boolean,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class CallInProgress(
|
||||
val participants: BaseVkGroupCall.Participants,
|
||||
val join_link: String
|
||||
val join_link: String,
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Participants(
|
||||
val list: List<Int>,
|
||||
val count: Int
|
||||
val count: Int,
|
||||
) : 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 InvalidDocTitle = 1152
|
||||
const val AccessToDocDenied = 1153
|
||||
|
||||
const val AccessTokenExpired = 1117
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
object VkErrors {
|
||||
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(
|
||||
@SerializedName("validation_type")
|
||||
@@ -76,3 +88,23 @@ data class CaptchaRequiredError(
|
||||
@SerializedName("captcha_img")
|
||||
val captchaImg: String
|
||||
) : 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.VKConstants
|
||||
import com.meloda.fast.api.network.account.AccountUrls
|
||||
import com.meloda.fast.api.network.ota.OtaUrls
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.net.URLEncoder
|
||||
@@ -14,7 +15,7 @@ class AuthInterceptor : Interceptor {
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ import java.lang.reflect.Type
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
class ResultCallFactory : CallAdapter.Factory() {
|
||||
class ResultCallFactory(private val gson: Gson) : CallAdapter.Factory() {
|
||||
override fun get(
|
||||
returnType: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
retrofit: Retrofit,
|
||||
): CallAdapter<*, *>? {
|
||||
val rawReturnType: Class<*> = getRawType(returnType)
|
||||
if (rawReturnType == Call::class.java) {
|
||||
@@ -27,9 +27,9 @@ class ResultCallFactory : CallAdapter.Factory() {
|
||||
if (getRawType(callInnerType) == ApiAnswer::class.java) {
|
||||
if (callInnerType is ParameterizedType) {
|
||||
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>
|
||||
}
|
||||
|
||||
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 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>>) {
|
||||
proxy.enqueue(ResultCallback(this, callback))
|
||||
proxy.enqueue(ResultCallback(this, callback, gson))
|
||||
}
|
||||
|
||||
override fun cloneImpl(): ResultCall<T> {
|
||||
return ResultCall(proxy.clone())
|
||||
return ResultCall(proxy.clone(), gson)
|
||||
}
|
||||
|
||||
private class ResultCallback<T>(
|
||||
private val proxy: ResultCall<T>,
|
||||
private val callback: Callback<ApiAnswer<T>>
|
||||
private val callback: Callback<ApiAnswer<T>>,
|
||||
private val gson: Gson
|
||||
) : Callback<T> {
|
||||
|
||||
val gson = Gson()
|
||||
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
val result: ApiAnswer<T> =
|
||||
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 {
|
||||
if (!result.isSuccessful()) {
|
||||
if (result.isError()) {
|
||||
result.error.throwable?.run {
|
||||
onFailure(call, this)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -143,8 +140,16 @@ sealed class ApiAnswer<out R> {
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun isSuccessful(): Boolean {
|
||||
contract {
|
||||
returns(false) implies (this@ApiAnswer is Error)
|
||||
returns(true) implies (this@ApiAnswer 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 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
|
||||
data class AuthDirectResponse(
|
||||
@SerializedName("access_token") val accessToken: String? = null,
|
||||
@SerializedName("user_id") val userId: Int? = null,
|
||||
@SerializedName("trusted_hash") val twoFaHash: String? = null,
|
||||
@SerializedName("validation_sid") val validationSid: String? = null
|
||||
@SerializedName("access_token") val accessToken: String?,
|
||||
@SerializedName("user_id") val userId: Int?,
|
||||
@SerializedName("trusted_hash") val twoFaHash: String?,
|
||||
@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
|
||||
|
||||
@Parcelize
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ package com.meloda.fast.api.network.conversations
|
||||
|
||||
import android.os.Parcelable
|
||||
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.BaseVkMessage
|
||||
import com.meloda.fast.api.model.base.BaseVkUser
|
||||
|
||||
@@ -189,5 +189,51 @@ data class MessagesGetByIdRequest(
|
||||
extended?.let { this["extended"] = it.intString }
|
||||
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
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.base.BaseVkConversation
|
||||
import com.meloda.fast.api.model.base.BaseVkGroup
|
||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||
import com.meloda.fast.api.model.base.BaseVkUser
|
||||
import com.meloda.fast.api.model.base.*
|
||||
import com.meloda.fast.api.model.data.BaseVkConversation
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -23,3 +21,11 @@ data class MessagesGetByIdResponse(
|
||||
val profiles: List<BaseVkUser>?,
|
||||
val groups: List<BaseVkGroup>?
|
||||
) : 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 GetById = "${VkUrls.API}/messages.getById"
|
||||
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(@LayoutRes resId: Int) : super(resId)
|
||||
|
||||
}
|
||||
@@ -1,47 +1,11 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.meloda.fast.screens.main.MainActivity
|
||||
|
||||
abstract class BaseFragment : Fragment {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
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 androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import com.meloda.fast.model.DataItem
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST")
|
||||
abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
||||
abstract class BaseAdapter<T : Any, VH : BaseHolder> constructor(
|
||||
var context: Context,
|
||||
diffUtil: DiffUtil.ItemCallback<T>,
|
||||
preAddedValues: List<T> = emptyList(),
|
||||
@@ -59,27 +58,18 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
||||
fun add(
|
||||
item: T,
|
||||
position: Int? = null,
|
||||
beforeFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) = addAll(listOf(item), position, beforeFooter, commitCallback)
|
||||
) = addAll(listOf(item), position, commitCallback)
|
||||
|
||||
fun addAll(
|
||||
items: List<T>,
|
||||
position: Int? = null,
|
||||
beforeFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) {
|
||||
adapterScope.launch {
|
||||
val newList = cloneCurrentList()
|
||||
if (position == null) {
|
||||
val mutableItems = items.toMutableList()
|
||||
if (beforeFooter && newList.lastOrNull() is DataItem.Footer) {
|
||||
newList.removeLastOrNull()
|
||||
}
|
||||
|
||||
if (beforeFooter) {
|
||||
mutableItems += DataItem.Footer as T
|
||||
}
|
||||
|
||||
newList.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) {
|
||||
val newList = cloneCurrentList()
|
||||
newList.removeAll(items)
|
||||
submitList(newList, commitCallback)
|
||||
|
||||
cleanList.removeAll(items)
|
||||
|
||||
submitList(newList, commitCallback)
|
||||
}
|
||||
|
||||
fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) {
|
||||
val newList = cloneCurrentList()
|
||||
newList.removeAt(index)
|
||||
submitList(newList, commitCallback)
|
||||
|
||||
cleanList.removeAt(index)
|
||||
|
||||
submitList(newList, commitCallback)
|
||||
}
|
||||
|
||||
fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback)
|
||||
|
||||
fun setItem(
|
||||
item: T,
|
||||
withHeader: Boolean = false,
|
||||
withFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) = setItems(listOf(item), withHeader, withFooter, commitCallback)
|
||||
) = setItems(listOf(item), commitCallback)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun setItems(
|
||||
list: List<T>?,
|
||||
withHeader: Boolean = false,
|
||||
withFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) {
|
||||
adapterScope.launch {
|
||||
val items = mutableListOf<T>()
|
||||
if (withHeader) items.add(DataItem.Header as T)
|
||||
if (!list.isNullOrEmpty()) items.addAll(list)
|
||||
if (withFooter) items.add(DataItem.Footer as T)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (items == currentList) {
|
||||
@@ -165,9 +149,9 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
||||
fun setItem(position: Int, item: T, commitCallback: (() -> Unit)? = null) {
|
||||
val newList = cloneCurrentList()
|
||||
newList[position] = item
|
||||
submitList(newList, commitCallback)
|
||||
|
||||
cleanList[position] = item
|
||||
|
||||
submitList(newList, commitCallback)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
fun interface OnItemClickListener<T> {
|
||||
fun onItemClick(item: T)
|
||||
}
|
||||
|
||||
interface OnItemLongClickListener {
|
||||
fun onItemLongClick(position: Int)
|
||||
fun interface OnItemLongClickListener<T> {
|
||||
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
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.fast.api.base.ApiError
|
||||
import com.meloda.fast.api.network.ApiAnswer
|
||||
import com.meloda.fast.api.network.AuthorizationError
|
||||
import com.meloda.fast.api.network.CaptchaRequiredError
|
||||
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
|
||||
import com.meloda.fast.api.network.*
|
||||
import com.meloda.fast.ext.isTrue
|
||||
import com.meloda.fast.ext.notNull
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
var unknownErrorDefaultText: String = ""
|
||||
open suspend fun sendSingleEvent(event: VkEvent) {}
|
||||
|
||||
protected val tasksEventChannel = Channel<VkEvent>()
|
||||
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
||||
suspend fun <T> sendRequestNotNull(
|
||||
onError: ErrorHandler? = null,
|
||||
request: suspend () -> ApiAnswer<T>
|
||||
): T = sendRequest(onError, request).notNull()
|
||||
|
||||
protected val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
viewModelScope.launch { onException(throwable) }
|
||||
}
|
||||
|
||||
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
|
||||
return viewModelScope.launch(exceptionHandler, block = block)
|
||||
}
|
||||
|
||||
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)
|
||||
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 -> {
|
||||
onError?.invoke(response.error) ?: checkErrors(response.error)
|
||||
}
|
||||
}
|
||||
val error = response.error
|
||||
|
||||
onEnd?.invoke()
|
||||
if (!onError?.handleError(error).isTrue) {
|
||||
checkErrors(error)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
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()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
when (throwable) {
|
||||
is TokenExpiredError -> {
|
||||
sendSingleEvent(TokenExpiredErrorEvent)
|
||||
}
|
||||
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 -> {
|
||||
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 -> {
|
||||
sendEvent(CaptchaRequiredEvent(throwable.captchaSid, throwable.captchaImg))
|
||||
sendSingleEvent(
|
||||
CaptchaRequiredEvent(
|
||||
sid = throwable.captchaSid,
|
||||
image = throwable.captchaImg
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is ApiError -> {
|
||||
sendEvent(ErrorTextEvent(errorText = throwable.errorMessage ?: unknownErrorDefaultText))
|
||||
sendSingleEvent(
|
||||
if (throwable.errorMessage == null) {
|
||||
UnknownErrorEvent
|
||||
} else {
|
||||
ErrorTextEvent(errorText = requireNotNull(throwable.errorMessage))
|
||||
}
|
||||
)
|
||||
}
|
||||
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.lifecycle.lifecycleScope
|
||||
import com.meloda.fast.base.BaseFragment
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
|
||||
@Deprecated("", ReplaceWith("BaseFragment"))
|
||||
abstract class BaseViewModelFragment<VM : DeprecatedBaseViewModel> : BaseFragment {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
@@ -25,7 +25,7 @@ abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
|
||||
ViewModelUtils.parseEvent(this, event)
|
||||
}
|
||||
|
||||
protected fun <T : BaseViewModel> subscribeToViewModel(viewModel: T) {
|
||||
protected fun <T : DeprecatedBaseViewModel> subscribeToViewModel(viewModel: T) {
|
||||
lifecycleScope.launch {
|
||||
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
|
||||
|
||||
abstract class VkEvent
|
||||
abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent()
|
||||
abstract class VkProgressEvent : VkEvent()
|
||||
import com.meloda.fast.model.base.UiText
|
||||
|
||||
abstract class VkEvent
|
||||
|
||||
abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent()
|
||||
|
||||
object UnknownErrorEvent : VkErrorEvent()
|
||||
open class ErrorTextEvent(override val errorText: String) : VkErrorEvent()
|
||||
|
||||
object AuthorizationErrorEvent : VkErrorEvent()
|
||||
object TokenExpiredErrorEvent : 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()
|
||||
object StopProgressEvent : VkProgressEvent()
|
||||
data class UserBannedEvent(
|
||||
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)
|
||||
}
|
||||
@@ -6,12 +6,13 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.base.BaseFragment
|
||||
import com.meloda.fast.screens.main.MainActivity
|
||||
import com.meloda.fast.util.ViewUtils.showErrorDialog
|
||||
import com.meloda.fast.ext.showDialog
|
||||
import com.meloda.fast.model.base.UiText
|
||||
import com.meloda.fast.screens.main.activity.MainActivity
|
||||
|
||||
object ViewModelUtils {
|
||||
|
||||
@Deprecated("rewrite")
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
fun parseEvent(activity: FragmentActivity, event: VkEvent) {
|
||||
when (event) {
|
||||
@@ -24,26 +25,47 @@ object ViewModelUtils {
|
||||
activity.finishAffinity()
|
||||
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 -> {
|
||||
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) {
|
||||
if (event is VkProgressEvent) {
|
||||
if (fragment is BaseFragment) {
|
||||
if (event is StartProgressEvent) {
|
||||
fragment.startProgress()
|
||||
} else if (event is StopProgressEvent) {
|
||||
fragment.stopProgress()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parseEvent(fragment.requireActivity(), event)
|
||||
}
|
||||
parseEvent(fragment.requireActivity(), event)
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,83 @@
|
||||
package com.meloda.fast.common
|
||||
|
||||
import android.app.Application
|
||||
import android.app.DownloadManager
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.net.ConnectivityManager
|
||||
import android.util.Log
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.media.AudioManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.room.Room
|
||||
import com.meloda.fast.database.AppDatabase
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.meloda.fast.common.di.applicationModule
|
||||
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.sqrt
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@HiltAndroidApp
|
||||
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() {
|
||||
super.onCreate()
|
||||
|
||||
instance = this
|
||||
|
||||
appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache")
|
||||
// .fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (preferences.getBoolean(
|
||||
SettingsFragment.KEY_USE_DYNAMIC_COLORS,
|
||||
SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS
|
||||
)
|
||||
) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
|
||||
val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
|
||||
versionName = info.versionName
|
||||
versionCode = PackageInfoCompat.getLongVersionCode(info).toInt()
|
||||
|
||||
Companion.resources = resources
|
||||
Companion.packageName = packageName
|
||||
Companion.packageManager = packageManager
|
||||
screenWidth80 = (AndroidUtils.getDisplayWidth() * 0.8).roundToInt()
|
||||
|
||||
screenWidth = resources.displayMetrics.widthPixels
|
||||
screenHeight = resources.displayMetrics.heightPixels
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
screenWidth80 = (screenWidth * 0.8).roundToInt()
|
||||
applyDarkTheme()
|
||||
|
||||
val density = resources.displayMetrics.density
|
||||
val densityDpi = resources.displayMetrics.densityDpi
|
||||
val densityScaled = resources.displayMetrics.scaledDensity
|
||||
val xDpi = resources.displayMetrics.xdpi
|
||||
val yDpi = resources.displayMetrics.ydpi
|
||||
initKoin()
|
||||
}
|
||||
|
||||
val diagonal = sqrt(
|
||||
(screenWidth * screenWidth - screenHeight * screenHeight).toFloat()
|
||||
private fun applyDarkTheme() {
|
||||
val nightMode = preferences.getInt(
|
||||
SettingsFragment.KEY_APPEARANCE_DARK_THEME,
|
||||
SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME
|
||||
)
|
||||
AppCompatDelegate.setDefaultNightMode(nightMode)
|
||||
}
|
||||
|
||||
Log.i(
|
||||
"Fast::DeviceInfo",
|
||||
"width: $screenWidth; 70% width: $screenWidth80; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
|
||||
)
|
||||
private fun initKoin() {
|
||||
startKoin {
|
||||
androidLogger()
|
||||
androidContext(this@AppGlobal)
|
||||
modules(applicationModule)
|
||||
}
|
||||
}
|
||||
|
||||
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
companion object {
|
||||
private lateinit var instance: AppGlobal
|
||||
|
||||
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
|
||||
|
||||
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.VkMessage
|
||||
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.screens.chatinfo.ChatInfoFragment
|
||||
import com.meloda.fast.screens.conversations.ConversationsFragment
|
||||
import com.meloda.fast.screens.login.LoginFragment
|
||||
import com.meloda.fast.screens.main.MainFragment
|
||||
import com.meloda.fast.screens.messages.ForwardedMessagesFragment
|
||||
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.userbanned.UserBannedFragment
|
||||
|
||||
@Suppress("FunctionName")
|
||||
object Screens {
|
||||
fun Main() = FragmentScreen { MainFragment() }
|
||||
fun Main() = FragmentScreen { MainFragment.newInstance() }
|
||||
|
||||
fun Login(
|
||||
getFastToken: Boolean = false
|
||||
) = FragmentScreen {
|
||||
LoginFragment.newInstance(getFastToken)
|
||||
}
|
||||
fun Login() = FragmentScreen { LoginFragment.newInstance() }
|
||||
|
||||
fun Conversations() = FragmentScreen { ConversationsFragment() }
|
||||
|
||||
fun MessagesHistory(
|
||||
conversation: VkConversation,
|
||||
conversation: VkConversationDomain,
|
||||
user: VkUser?,
|
||||
group: VkGroup?
|
||||
) = FragmentScreen { MessagesHistoryFragment.newInstance(conversation, user, group) }
|
||||
|
||||
fun ForwardedMessages(
|
||||
conversation: VkConversation,
|
||||
conversation: VkConversationDomain,
|
||||
messages: List<VkMessage>,
|
||||
profiles: HashMap<Int, VkUser> = 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) =
|
||||
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
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.meloda.fast.BuildConfig
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import com.meloda.fast.api.network.ApiAnswer
|
||||
import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse
|
||||
import com.meloda.fast.data.ota.OtaApi
|
||||
import com.meloda.fast.extensions.setIfNotEquals
|
||||
import com.meloda.fast.model.UpdateActualUrl
|
||||
import com.meloda.fast.model.UpdateItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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.withContext
|
||||
import java.net.URLEncoder
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class UpdateManager(private val repo: OtaApi) : CoroutineScope {
|
||||
interface UpdateManager {
|
||||
val stateFlow: Flow<UpdateManagerState>
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default
|
||||
fun checkUpdates(): Job
|
||||
}
|
||||
|
||||
companion object {
|
||||
val newUpdate = MutableLiveData<UpdateItem?>(null)
|
||||
val updateError = MutableLiveData<Throwable?>(null)
|
||||
class UpdateManagerImpl(private val repo: OtaApi) : UpdateManager {
|
||||
|
||||
var otaBaseUrl: String? = null
|
||||
private set
|
||||
}
|
||||
private val coroutineContext: CoroutineContext
|
||||
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() }
|
||||
|
||||
when (val jobResponse = job()) {
|
||||
@@ -44,47 +46,55 @@ class UpdateManager(private val repo: OtaApi) : CoroutineScope {
|
||||
is ApiAnswer.Error -> {
|
||||
otaBaseUrl = null
|
||||
val throwable = jobResponse.error.throwable
|
||||
listener?.invoke(null, throwable)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateError.setIfNotEquals(throwable)
|
||||
}
|
||||
val newForm = stateFlow.value.copy(
|
||||
updateItem = null,
|
||||
throwable = throwable
|
||||
)
|
||||
stateFlow.emit(newForm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLatestRelease() = launch {
|
||||
private fun getLatestRelease() = coroutineScope.launch {
|
||||
val url = "$otaBaseUrl/releases-latest"
|
||||
|
||||
val job: suspend () -> ApiAnswer<ApiResponse<OtaGetLatestReleaseResponse>> = {
|
||||
repo.getLatestRelease(url = url, secretCode = getOtaSecret())
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
when (val jobResponse = job()) {
|
||||
is ApiAnswer.Success -> {
|
||||
val response = jobResponse.data.response ?: return@withContext
|
||||
val latestRelease = response.release
|
||||
when (val jobResponse = job()) {
|
||||
is ApiAnswer.Success -> {
|
||||
val response = jobResponse.data.response ?: return@launch
|
||||
val latestRelease = response.release
|
||||
|
||||
if (latestRelease != null &&
|
||||
(AppGlobal.versionName
|
||||
.split("_")
|
||||
.getOrNull(1) != latestRelease.versionName ||
|
||||
AppGlobal.versionCode < latestRelease.versionCode)
|
||||
) {
|
||||
newUpdate.setIfNotEquals(latestRelease)
|
||||
listener?.invoke(latestRelease, null)
|
||||
} else {
|
||||
newUpdate.setIfNotEquals(null)
|
||||
listener?.invoke(null, null)
|
||||
}
|
||||
val updateItem = if (latestRelease != null &&
|
||||
(AppGlobal.versionName
|
||||
.split("_")
|
||||
.getOrNull(1) != latestRelease.versionName ||
|
||||
AppGlobal.versionCode < latestRelease.versionCode)
|
||||
) {
|
||||
latestRelease
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
is ApiAnswer.Error -> {
|
||||
val throwable = jobResponse.error.throwable
|
||||
updateError.setIfNotEquals(throwable)
|
||||
listener?.invoke(null, throwable)
|
||||
}
|
||||
val newForm = stateFlow.value.copy(
|
||||
updateItem = updateItem,
|
||||
throwable = null
|
||||
)
|
||||
|
||||
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 {
|
||||
return URLEncoder.encode(BuildConfig.otaSecretCode, "utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
fun checkUpdates(block: ((item: UpdateItem?, error: Throwable?) -> Unit)? = null) = launch {
|
||||
this@UpdateManager.listener = block
|
||||
getActualUrl()
|
||||
data class UpdateManagerState(
|
||||
val updateItem: UpdateItem?,
|
||||
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)
|
||||
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.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||
|
||||
@Dao
|
||||
interface ConversationsDao {
|
||||
|
||||
@Query("SELECT * FROM conversations")
|
||||
suspend fun getAll(): List<VkConversation>
|
||||
suspend fun getAll(): List<VkConversationDomain>
|
||||
|
||||
@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
|
||||
|
||||
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.ConversationsGetRequest
|
||||
import com.meloda.fast.api.network.conversations.ConversationsPinRequest
|
||||
import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
||||
class ConversationsRepository(
|
||||
private val conversationsApi: ConversationsApi,
|
||||
@@ -20,6 +19,6 @@ class ConversationsRepository(
|
||||
|
||||
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
|
||||
|
||||
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.BaseVkMessage
|
||||
import com.meloda.fast.api.network.ApiAnswer
|
||||
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.MessagesUrls
|
||||
import retrofit2.http.FieldMap
|
||||
@@ -53,4 +55,16 @@ interface MessagesApi {
|
||||
@POST(MessagesUrls.MarkAsRead)
|
||||
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.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.api.network.messages.*
|
||||
|
||||
class MessagesRepository(
|
||||
private val messagesApi: MessagesApi,
|
||||
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 getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
|
||||
@@ -41,7 +54,7 @@ class MessagesRepository(
|
||||
|
||||
suspend fun getLongPollUpdates(
|
||||
serverUrl: String,
|
||||
params: LongPollGetUpdatesRequest
|
||||
params: LongPollGetUpdatesRequest,
|
||||
) = longPollApi.getResponse(serverUrl, params.map)
|
||||
|
||||
suspend fun getById(params: MessagesGetByIdRequest) =
|
||||
@@ -50,7 +63,7 @@ class MessagesRepository(
|
||||
suspend fun markAsRead(
|
||||
peerId: Int,
|
||||
messagesIds: List<Int>? = null,
|
||||
startMessageId: Int? = null
|
||||
startMessageId: Int? = null,
|
||||
) = messagesApi.markAsRead(
|
||||
mutableMapOf("peer_id" to peerId.toString()).apply {
|
||||
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
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
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.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.groups.GroupsDao
|
||||
import com.meloda.fast.data.messages.MessagesDao
|
||||
import com.meloda.fast.data.users.UsersDao
|
||||
import com.meloda.fast.model.AppAccount
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
AppAccount::class,
|
||||
VkConversation::class,
|
||||
VkConversationDomain::class,
|
||||
VkMessage::class,
|
||||
VkUser::class,
|
||||
VkGroup::class
|
||||
],
|
||||
version = 34,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 33, to = 34)
|
||||
]
|
||||
version = 42,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract class CacheDatabase : RoomDatabase() {
|
||||
|
||||
abstract val accountsDao: AccountsDao
|
||||
abstract val conversationsDao: ConversationsDao
|
||||
abstract val messagesDao: MessagesDao
|
||||
abstract val usersDao: UsersDao
|
||||
@@ -2,6 +2,7 @@ package com.meloda.fast.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
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.attachments.VkAttachment
|
||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||
@@ -18,25 +19,37 @@ class Converters {
|
||||
fun fromGeoToString(geo: BaseVkMessage.Geo?): String? {
|
||||
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
|
||||
fun fromStringToGeo(string: String?): BaseVkMessage.Geo? {
|
||||
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
|
||||
fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
|
||||
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
|
||||
}
|
||||
@@ -46,40 +59,52 @@ class Converters {
|
||||
if (string == null) return null
|
||||
|
||||
if (string.contains(CACHE_SEPARATOR)) {
|
||||
val messages =
|
||||
string.split(CACHE_SEPARATOR).map { fromStringToVkMessage(it)!! }
|
||||
val messages = string
|
||||
.split(CACHE_SEPARATOR)
|
||||
.mapNotNull(::fromStringToVkMessage)
|
||||
return messages
|
||||
}
|
||||
|
||||
|
||||
val message = fromStringToVkMessage(string)!!
|
||||
|
||||
return listOf(message)
|
||||
val message = fromStringToVkMessage(string)
|
||||
return message?.let { listOf(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromVkMessageToString(message: VkMessage?): String? {
|
||||
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
|
||||
fun fromStringToVkMessage(string: String?): VkMessage? {
|
||||
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
|
||||
fun fromListVkAttachmentToString(attachments: List<VkAttachment>?): String? {
|
||||
if (attachments == null) return null
|
||||
|
||||
val string =
|
||||
attachments.map { fromVkAttachmentToString(it)!! }.joinToString { CACHE_SEPARATOR }
|
||||
|
||||
val string = attachments
|
||||
.mapNotNull(::fromVkAttachmentToString)
|
||||
.joinToString(separator = CACHE_SEPARATOR)
|
||||
return string
|
||||
}
|
||||
|
||||
@@ -88,34 +113,48 @@ class Converters {
|
||||
if (string == null) return null
|
||||
|
||||
if (string.contains(CACHE_SEPARATOR)) {
|
||||
val attachments =
|
||||
string.split(CACHE_SEPARATOR).map { fromStringToVkAttachment(it)!! }
|
||||
val attachments = string
|
||||
.split(CACHE_SEPARATOR)
|
||||
.mapNotNull(::fromStringToVkAttachment)
|
||||
return attachments
|
||||
}
|
||||
|
||||
val attachment = fromStringToVkAttachment(string)
|
||||
|
||||
val attachment = fromStringToVkAttachment(string)!!
|
||||
|
||||
return listOf(attachment)
|
||||
return attachment?.let { listOf(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromVkAttachmentToString(attachment: VkAttachment?): String? {
|
||||
if (attachment == null) return null
|
||||
|
||||
val string = Gson().toJson(attachment)
|
||||
|
||||
return string
|
||||
try {
|
||||
attachment.javaClass.getDeclaredField("className")
|
||||
} catch (e: NoSuchFieldException) {
|
||||
throw AttachmentClassNameIsEmptyException(attachment)
|
||||
}
|
||||
return try {
|
||||
val string = Gson().toJson(attachment)
|
||||
string
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
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
|
||||
|
||||
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.audios.AudiosApi
|
||||
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.conversations.ConversationsApi
|
||||
import com.meloda.fast.data.conversations.ConversationsDao
|
||||
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.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.photos.PhotosApi
|
||||
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.videos.VideosApi
|
||||
import com.meloda.fast.data.videos.VideosRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
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)
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
// 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
|
||||
|
||||
import androidx.room.Room
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.data.account.AccountsDao
|
||||
import com.meloda.fast.data.conversations.ConversationsDao
|
||||
import com.meloda.fast.data.groups.GroupsDao
|
||||
import com.meloda.fast.data.messages.MessagesDao
|
||||
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
|
||||
import com.meloda.fast.database.AccountsDatabase
|
||||
import com.meloda.fast.database.CacheDatabase
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
|
||||
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.Router
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import com.meloda.fast.screens.captcha.screen.CaptchaScreen
|
||||
import com.meloda.fast.screens.twofa.screen.TwoFaScreen
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object NavigationModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getCicerone(): Cicerone<Router> = Cicerone.create()
|
||||
val navigationModule = module {
|
||||
single { Cicerone.create() }
|
||||
single { cicerone().router }
|
||||
single { cicerone().getNavigatorHolder() }
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getRouter(cicerone: Cicerone<Router>) = cicerone.router
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getNavigationHolder(cicerone: Cicerone<Router>) = cicerone.getNavigatorHolder()
|
||||
singleOf(::CaptchaScreen)
|
||||
singleOf(::TwoFaScreen)
|
||||
}
|
||||
|
||||
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.ChuckerInterceptor
|
||||
import com.google.gson.Gson
|
||||
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.ResultCallFactory
|
||||
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.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.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object NetworkModule {
|
||||
|
||||
/*
|
||||
|
||||
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 =
|
||||
val networkModule = module {
|
||||
single { ChuckerCollector(get()) }
|
||||
single { ChuckerInterceptor.Builder(get()).collector(get()).build() }
|
||||
singleOf(::AuthInterceptor)
|
||||
single { GsonBuilder().setLenient().create() }
|
||||
single {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(chuckerInterceptor)
|
||||
.addInterceptor(authInterceptor())
|
||||
.addInterceptor(
|
||||
chuckerInterceptor().apply {
|
||||
redactHeader("Secret-Code")
|
||||
}
|
||||
)
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.addInterceptor(
|
||||
@@ -99,92 +37,17 @@ val chuckerInterceptor = ChuckerInterceptor.Builder(context)
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
).build()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideGson(): Gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
@Singleton
|
||||
@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)
|
||||
|
||||
}
|
||||
single {
|
||||
Retrofit.Builder()
|
||||
.baseUrl("${VkUrls.API}/")
|
||||
.addConverterFactory(GsonConverterFactory.create(get()))
|
||||
.addCallAdapterFactory(ResultCallFactory(get()))
|
||||
.client(get())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
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.drawable.ColorDrawable
|
||||
@@ -27,71 +27,85 @@ object ImageLoader {
|
||||
this.setImageDrawable(null)
|
||||
}
|
||||
|
||||
fun ImageView.loadWithGlide(
|
||||
url: String? = null,
|
||||
uri: Uri? = null,
|
||||
drawableRes: Int? = null,
|
||||
drawable: Drawable? = null,
|
||||
placeholderDrawable: Drawable? = null,
|
||||
placeholderColor: Int? = null,
|
||||
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
|
||||
) {
|
||||
fun ImageView.loadWithGlide(block: GlideParams.() -> Unit) {
|
||||
val params = GlideParams()
|
||||
block.invoke(params)
|
||||
loadWithGlide(params)
|
||||
}
|
||||
|
||||
fun ImageView.loadWithGlide(params: GlideParams) {
|
||||
val request = Glide.with(this)
|
||||
|
||||
var builder = when {
|
||||
url != null -> request.load(url)
|
||||
uri != null -> request.load(uri)
|
||||
drawableRes != null -> request.load(drawableRes)
|
||||
params.imageUrl != null -> request.load(params.imageUrl)
|
||||
params.imageUri != null -> request.load(params.imageUri)
|
||||
params.drawableRes != null -> request.load(params.drawableRes)
|
||||
drawable != null -> request.load(drawable)
|
||||
else -> request.load(null as Drawable?)
|
||||
}
|
||||
|
||||
val transforms = transformations.toMutableList()
|
||||
if (asCircle) {
|
||||
val transforms = params.transformations.toMutableList()
|
||||
if (params.asCircle) {
|
||||
transforms += TypeTransformations.CircleCrop
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.apply(TypeTransformations.createRequestOptions(transforms))
|
||||
.error(
|
||||
errorDrawable
|
||||
?: if (errorColor != null) ColorDrawable(errorColor) else null
|
||||
params.errorDrawable
|
||||
?: if (params.errorColor != null) {
|
||||
ColorDrawable(requireNotNull(params.errorColor))
|
||||
} else null
|
||||
)
|
||||
.placeholder(
|
||||
placeholderDrawable
|
||||
?: if (placeholderColor != null) ColorDrawable(placeholderColor) else null
|
||||
params.placeholderDrawable
|
||||
?: if (params.placeholderColor != null) {
|
||||
ColorDrawable(requireNotNull(params.placeholderColor))
|
||||
} else null
|
||||
)
|
||||
.addListener(ImageLoadRequestListener(onLoadedAction, onFailedAction))
|
||||
.diskCacheStrategy(cacheStrategy)
|
||||
.priority(priority)
|
||||
.addListener(ImageLoadRequestListener(params.onLoadedAction, params.onFailedAction))
|
||||
.addListener(ImageLoadDoneListener(params.onDoneAction))
|
||||
.diskCacheStrategy(params.cacheStrategy)
|
||||
.priority(params.loadPriority)
|
||||
|
||||
if (crossFade || crossFadeDuration != null) {
|
||||
builder = builder.transition(withCrossFade(crossFadeDuration ?: 200))
|
||||
if (params.crossFade || params.crossFadeDuration != null) {
|
||||
builder = builder.transition(withCrossFade(params.crossFadeDuration ?: 200))
|
||||
}
|
||||
|
||||
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(
|
||||
private val onLoadedAction: (() -> Unit)?,
|
||||
private val onFailedAction: (() -> Unit)?
|
||||
private val onFailedAction: (() -> Unit)?,
|
||||
) : RequestListener<Drawable> {
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
onFailedAction?.invoke()
|
||||
return false
|
||||
@@ -102,13 +116,36 @@ class ImageLoadRequestListener(
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
onLoadedAction?.invoke()
|
||||
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 {
|
||||
|
||||
object CenterCrop : TypeTransformations()
|
||||
@@ -123,7 +160,7 @@ sealed class TypeTransformations {
|
||||
val topLeft: Float,
|
||||
val topRight: Float,
|
||||
val bottomRight: Float,
|
||||
val bottomLeft: Float
|
||||
val bottomLeft: Float,
|
||||
) : TypeTransformations()
|
||||
|
||||
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
|
||||
|
||||
@Parcelize
|
||||
open class SelectableItem constructor(
|
||||
@Ignore
|
||||
val selectableItemId: Int = 0
|
||||
) : DataItem<Int>(), Parcelable {
|
||||
open class SelectableItem : Parcelable {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var isSelected: Boolean = false
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
override val dataItemId = selectableItemId
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ data class UpdateItem(
|
||||
val originalName: String,
|
||||
val fileSize: Int,
|
||||
val preRelease: Int,
|
||||
val downloadLink: String
|
||||
val downloadLink: String,
|
||||
) : Parcelable {
|
||||
|
||||
fun isMandatory(): Boolean = mandatory == 1
|
||||
@@ -29,6 +29,24 @@ data class UpdateItem(
|
||||
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
|
||||
|
||||
@@ -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