Compare commits
91 Commits
0.2.2
...
7b2c102470
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b2c102470 | |||
| 5310596cf6 | |||
| 65ff74622a | |||
| f48878f003 | |||
| c666bd46f3 | |||
| 421ca27758 | |||
| 8231062ca5 | |||
| 723555f634 | |||
| 3e05744a18 | |||
| 821ee46cef | |||
| dcddddea9b | |||
| 018151ad18 | |||
| 6f55251fb7 | |||
| 83772fb3ec | |||
| d5ee50a979 | |||
| 079c4178ec | |||
| 21bcacded8 | |||
| 3a272376c1 | |||
| ea6c094b4d | |||
| 91b87f6fa5 | |||
| a5952098a2 | |||
| 4f1149c8d8 | |||
| 56bf6ac063 | |||
| 1b9401673d | |||
| db92b261c0 | |||
| 42ad04093d | |||
| 9535847bf3 | |||
| b3c35ec6f5 | |||
| f5853f61c3 | |||
| 620cf53c65 | |||
| 411776b767 | |||
| 8cb3ed8784 | |||
| 4677e484d9 | |||
| 799ed820e3 | |||
| 3fd679e65d | |||
| 6b95deb7bf | |||
| 47c1f623f0 | |||
| 600aed40e7 | |||
| 22e8a5c09e | |||
| 252f6ec21e | |||
| 7e25bc3a8d | |||
| 7b0f6fe2a6 | |||
| 6d5b09ef81 | |||
| 268e0a8beb | |||
| 9e67ad0834 | |||
| 34ea8ef944 | |||
| 3d0c310575 | |||
| 602cf8f18b | |||
| 1b3581fcfd | |||
| 4b254d7d41 | |||
| d98dca83f1 | |||
| 9e6b079bf6 | |||
| d2aaac68e2 | |||
| ada9c13947 | |||
| cd4c1d6d76 | |||
| fab92d50d6 | |||
| 9e4521c3a0 | |||
| 70b552412c | |||
| a7307e7862 | |||
| 60a30b9422 | |||
| 93d81f1e9e | |||
| 5be101deec | |||
| 76dd1e2ce7 | |||
| 56683bea96 | |||
| 3dae1fe101 | |||
| c1e76e1c60 | |||
| c14ee45d53 | |||
| 9d4e3f50ea | |||
| 17b5c944ac | |||
| 5aa1f21183 | |||
| a916dc649c | |||
| 7e0b9d49ba | |||
| d525a1573c | |||
| 091a88dd45 | |||
| a3a970115a | |||
| e36c1ea0ca | |||
| f2437f67cb | |||
| 049bcfd2da | |||
| d5e24214ce | |||
| c94e128e64 | |||
| 0836816391 | |||
| be58949372 | |||
| 618388a719 | |||
| 7369fd5c70 | |||
| b668e86622 | |||
| aa4326f09e | |||
| 0a8d26a8f0 | |||
| 08d20f7a0f | |||
| 325211ad5f | |||
| b63cc86e48 | |||
| 628b93e4ab |
+28
-14
@@ -1,10 +1,10 @@
|
||||
name: Android CI Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "hotfix/*", "feature/*" ]
|
||||
pull_request:
|
||||
branches: [ "master", "hotfix/*", "feature/*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
name: Build artifacts
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
@@ -32,17 +32,31 @@ jobs:
|
||||
- name: Build and sign release APK
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Upload release APK
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Find generated release APK name
|
||||
id: find_apk_release
|
||||
run: |
|
||||
APK_PATH=$(find app/build/outputs/apk/release -name "*.apk" | head -n 1)
|
||||
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
|
||||
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK with original name
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: app-release.apk
|
||||
path: app/build/outputs/apk/release/app-release.apk
|
||||
name: ${{ env.APK_NAME }}
|
||||
path: ${{ env.APK_PATH }}
|
||||
|
||||
- name: Build and sign debug APK
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- name: Upload debug APK
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Find generated debug APK name
|
||||
id: find_apk_debug
|
||||
run: |
|
||||
APK_PATH=$(find app/build/outputs/apk/debug -name "*.apk" | head -n 1)
|
||||
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
|
||||
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK with original name
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: app-debug.apk
|
||||
path: app/build/outputs/apk/debug/app-debug.apk
|
||||
name: ${{ env.APK_NAME }}
|
||||
path: ${{ env.APK_PATH }}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: Android CI Release
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "release/*"]
|
||||
|
||||
@@ -15,10 +18,10 @@ jobs:
|
||||
name: Build artifacts
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
@@ -31,7 +34,7 @@ jobs:
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Upload release APK
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: app-release.apk
|
||||
path: app/build/outputs/apk/release/app-release.apk
|
||||
@@ -40,7 +43,7 @@ jobs:
|
||||
run: ./gradlew bundleRelease
|
||||
|
||||
- name: Upload release Bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: app-release.aab
|
||||
path: app/build/outputs/bundle/release/app-release.aab
|
||||
|
||||
@@ -7,15 +7,17 @@ Unofficial messenger for russian social network VKontakte
|
||||
- [x] 2FA support
|
||||
- [x] Resend otp
|
||||
- [x] Captcha support
|
||||
- [ ] Support for new authorization with service and refresh tokens
|
||||
- [x] Support for new authorization with service and refresh tokens
|
||||
- [ ] Handle token expiration
|
||||
- [x] Ability to export/import tokens
|
||||
- [x] Conversations list
|
||||
- [x] Pagination
|
||||
- [x] Manual refresh
|
||||
- [x] Pin & unpin conversations
|
||||
- [x] Delete conversations
|
||||
- [ ] Archive
|
||||
- [ ] View archived conversations
|
||||
- [ ] Archive & unarchive conversations
|
||||
- [x] Archive
|
||||
- [x] View archived conversations
|
||||
- [x] Archive & unarchive conversations
|
||||
- [x] Friends list
|
||||
- [x] Sort alphabetically, by priority or random
|
||||
- [x] Separate tab with only friends who are online
|
||||
@@ -30,17 +32,24 @@ Unofficial messenger for russian social network VKontakte
|
||||
- [x] Read status
|
||||
- [x] Edit status
|
||||
- [x] Sending status
|
||||
- [ ] Message's attachments
|
||||
- [ ] Photo
|
||||
- [ ] Video
|
||||
- [ ] Audio
|
||||
- [ ] File
|
||||
- [ ] Link
|
||||
- [x] Message's attachments
|
||||
- [x] Photo
|
||||
- [x] Video
|
||||
- [x] Audio
|
||||
- [x] File
|
||||
- [x] Link
|
||||
- [x] Sticker
|
||||
- [x] Reply
|
||||
- [ ] Forwarded messages
|
||||
- [ ] Wall post
|
||||
- [ ] Comment in wall post
|
||||
- [ ] Poll
|
||||
- [ ] TODO
|
||||
- [x] Send messages
|
||||
- [x] Pinned message
|
||||
- [x] Pin & unpin messages
|
||||
- [ ] Reply to message
|
||||
- [x] Reply to message
|
||||
- [x] Swipe to reply to message
|
||||
- [x] Delete message
|
||||
- [x] Select multiple messages
|
||||
- [x] Delete
|
||||
@@ -55,7 +64,7 @@ Unofficial messenger for russian social network VKontakte
|
||||
- [x] View attachments
|
||||
- [x] Open photo
|
||||
- [x] Internal viewer
|
||||
- [ ] External viewer
|
||||
- [x] External viewer
|
||||
- [ ] Open video in external player
|
||||
- [ ] TODO
|
||||
- [ ] Caching
|
||||
|
||||
+15
-2
@@ -1,3 +1,4 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
@@ -12,8 +13,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "dev.meloda.fastvk"
|
||||
|
||||
versionCode = libs.versions.versionCode.get().toInt()
|
||||
versionName = libs.versions.versionName.get()
|
||||
versionCode = 10
|
||||
versionName = "0.2.2"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -58,6 +59,18 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.all {
|
||||
val date = System.currentTimeMillis() / 1000
|
||||
val buildType = buildType.name
|
||||
val appVersion = versionName
|
||||
val appVersionCode = versionCode
|
||||
|
||||
val newApkName = "app-$buildType-v$appVersion($appVersionCode)-$date.apk"
|
||||
(this as? BaseVariantOutputImpl)?.outputFileName = newApkName
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
|
||||
Binary file not shown.
@@ -5,6 +5,7 @@ import android.content.res.Resources
|
||||
import android.os.PowerManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil.ImageLoader
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import dev.meloda.fast.MainViewModelImpl
|
||||
import dev.meloda.fast.auth.captcha.di.captchaModule
|
||||
import dev.meloda.fast.auth.login.di.loginModule
|
||||
@@ -33,6 +34,7 @@ import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
val applicationModule = module {
|
||||
includes(domainModule)
|
||||
includes(
|
||||
@@ -66,6 +68,7 @@ val applicationModule = module {
|
||||
ImageLoader.Builder(get())
|
||||
.crossfade(true)
|
||||
.build()
|
||||
.also { it.diskCache?.directory?.toFile()?.listFiles() }
|
||||
}
|
||||
|
||||
singleOf(::LongPollControllerImpl) bind LongPollController::class
|
||||
|
||||
@@ -10,7 +10,7 @@ import dev.meloda.fast.presentation.MainScreen
|
||||
import dev.meloda.fast.profile.navigation.Profile
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlinx.serialization.Serializable
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
@Serializable
|
||||
object MainGraph
|
||||
@@ -28,21 +28,21 @@ fun NavGraphBuilder.mainScreen(
|
||||
) {
|
||||
val navigationItems = ImmutableList.of(
|
||||
BottomNavigationItem(
|
||||
titleResId = UiR.string.title_friends,
|
||||
selectedIconResId = UiR.drawable.baseline_people_alt_24,
|
||||
unselectedIconResId = UiR.drawable.outline_people_alt_24,
|
||||
titleResId = R.string.title_friends,
|
||||
selectedIconResId = R.drawable.baseline_people_alt_24,
|
||||
unselectedIconResId = R.drawable.outline_people_alt_24,
|
||||
route = Friends,
|
||||
),
|
||||
BottomNavigationItem(
|
||||
titleResId = UiR.string.title_conversations,
|
||||
selectedIconResId = UiR.drawable.baseline_chat_24,
|
||||
unselectedIconResId = UiR.drawable.outline_chat_24,
|
||||
titleResId = R.string.title_conversations,
|
||||
selectedIconResId = R.drawable.baseline_chat_24,
|
||||
unselectedIconResId = R.drawable.outline_chat_24,
|
||||
route = ConversationsGraph
|
||||
),
|
||||
BottomNavigationItem(
|
||||
titleResId = UiR.string.title_profile,
|
||||
selectedIconResId = UiR.drawable.baseline_account_circle_24,
|
||||
unselectedIconResId = UiR.drawable.outline_account_circle_24,
|
||||
titleResId = R.string.title_profile,
|
||||
selectedIconResId = R.drawable.baseline_account_circle_24,
|
||||
unselectedIconResId = R.drawable.outline_account_circle_24,
|
||||
route = Profile
|
||||
)
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -41,18 +41,17 @@ import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.service.OnlineService
|
||||
import dev.meloda.fast.service.longpolling.LongPollingService
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.common.LocalSizeConfig
|
||||
import dev.meloda.fast.ui.model.DeviceSize
|
||||
import dev.meloda.fast.ui.model.SizeConfig
|
||||
import dev.meloda.fast.ui.model.ThemeConfig
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.theme.LocalUser
|
||||
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.KoinContext
|
||||
import org.koin.compose.koinInject
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -89,8 +88,7 @@ class MainActivity : AppCompatActivity() {
|
||||
requestNotificationPermissions()
|
||||
|
||||
setContent {
|
||||
KoinContext {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
val longPollController: LongPollController = koinInject()
|
||||
@@ -166,10 +164,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
val deviceWidthDp = remember(true) {
|
||||
context.resources.displayMetrics.widthPixels.pxToDp()
|
||||
resources.displayMetrics.widthPixels.pxToDp()
|
||||
}
|
||||
val deviceHeightDp = remember(true) {
|
||||
context.resources.displayMetrics.heightPixels.pxToDp()
|
||||
resources.displayMetrics.heightPixels.pxToDp()
|
||||
}
|
||||
|
||||
val deviceWidthSize by remember(deviceWidthDp) {
|
||||
@@ -253,13 +251,12 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
|
||||
val noCategoryName = getString(R.string.notification_channel_no_category_name)
|
||||
val noCategoryDescriptionText =
|
||||
getString(UiR.string.notification_channel_no_category_description)
|
||||
getString(R.string.notification_channel_no_category_description)
|
||||
val noCategoryChannel =
|
||||
NotificationChannel(
|
||||
AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
|
||||
@@ -269,9 +266,9 @@ class MainActivity : AppCompatActivity() {
|
||||
description = noCategoryDescriptionText
|
||||
}
|
||||
|
||||
val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
|
||||
val longPollName = getString(R.string.notification_channel_long_polling_service_name)
|
||||
val longPollDescriptionText =
|
||||
getString(UiR.string.notification_channel_long_polling_service_description)
|
||||
getString(R.string.notification_channel_long_polling_service_description)
|
||||
val longPollChannel =
|
||||
NotificationChannel(
|
||||
AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dev.meloda.fast.presentation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -16,7 +18,6 @@ import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -37,7 +38,6 @@ import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.conversations.navigation.Conversations
|
||||
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
||||
import dev.meloda.fast.conversations.navigation.conversationsGraph
|
||||
import dev.meloda.fast.friends.navigation.Friends
|
||||
@@ -65,8 +65,9 @@ fun MainScreen(
|
||||
onMessageClicked: (userid: Long) -> Unit = {},
|
||||
onNavigateToCreateChat: () -> Unit = {}
|
||||
) {
|
||||
val activity = LocalActivity.current as? AppCompatActivity ?: return
|
||||
val theme = LocalThemeConfig.current
|
||||
val hazeState = remember { HazeState() }
|
||||
val hazeState = remember { HazeState(true) }
|
||||
val navController = rememberNavController()
|
||||
|
||||
var selectedItemIndex by rememberSaveable {
|
||||
@@ -74,28 +75,20 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
BackHandler(enabled = selectedItemIndex != 1) {
|
||||
val index = 1
|
||||
val currentRoute = navigationItems[selectedItemIndex].route
|
||||
|
||||
selectedItemIndex = 1
|
||||
navController.navigate(navigationItems[index].route) {
|
||||
navController.navigate(navigationItems[selectedItemIndex].route) {
|
||||
popUpTo(route = currentRoute) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val user = LocalUser.current
|
||||
val profileImageUrl by remember(user) {
|
||||
derivedStateOf { user?.photo100 }
|
||||
}
|
||||
val profileImageUrl = LocalUser.current?.photo100
|
||||
|
||||
var tabReselected by remember {
|
||||
mutableStateOf(
|
||||
navigationItems.associate {
|
||||
it.route to false
|
||||
}
|
||||
)
|
||||
mutableStateOf(navigationItems.associate { it.route to false })
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -107,7 +100,7 @@ fun MainScreen(
|
||||
if (theme.enableBlur) {
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
style = HazeMaterials.regular(NavigationBarDefaults.containerColor)
|
||||
)
|
||||
} else Modifier
|
||||
),
|
||||
@@ -194,6 +187,7 @@ fun MainScreen(
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
friendsScreen(
|
||||
activity = activity,
|
||||
onError = onError,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
@@ -204,6 +198,7 @@ fun MainScreen(
|
||||
},
|
||||
)
|
||||
conversationsGraph(
|
||||
activity = activity,
|
||||
onError = onError,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
onNavigateToCreateChat = onNavigateToCreateChat,
|
||||
@@ -214,6 +209,7 @@ fun MainScreen(
|
||||
}
|
||||
)
|
||||
profileScreen(
|
||||
activity = activity,
|
||||
onError = onError,
|
||||
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
|
||||
@@ -3,9 +3,12 @@ package dev.meloda.fast.presentation
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -13,6 +16,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
@@ -34,8 +41,7 @@ import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
|
||||
import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory
|
||||
import dev.meloda.fast.navigation.Main
|
||||
import dev.meloda.fast.navigation.mainScreen
|
||||
import dev.meloda.fast.photoviewer.navigation.navigateToPhotoView
|
||||
import dev.meloda.fast.photoviewer.navigation.photoViewScreen
|
||||
import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog
|
||||
import dev.meloda.fast.settings.navigation.navigateToSettings
|
||||
import dev.meloda.fast.settings.navigation.settingsScreen
|
||||
import dev.meloda.fast.ui.R
|
||||
@@ -47,6 +53,7 @@ fun RootScreen(
|
||||
navController: NavHostController = rememberNavController(),
|
||||
viewModel: MainViewModel
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
val context = LocalContext.current
|
||||
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle()
|
||||
@@ -118,6 +125,11 @@ fun RootScreen(
|
||||
LocalNavRootController provides navController,
|
||||
LocalNavController provides navController
|
||||
) {
|
||||
var photoViewerInfo by rememberSaveable {
|
||||
mutableStateOf<Pair<List<String>, Int?>?>(null)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = requireNotNull(startDestination),
|
||||
@@ -129,6 +141,7 @@ fun RootScreen(
|
||||
viewModel.onUserAuthenticated()
|
||||
navController.navigateToMain()
|
||||
},
|
||||
onNavigateToSettings = navController::navigateToSettings,
|
||||
navController = navController
|
||||
)
|
||||
|
||||
@@ -136,7 +149,7 @@ fun RootScreen(
|
||||
onError = viewModel::onError,
|
||||
onSettingsButtonClicked = navController::navigateToSettings,
|
||||
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
||||
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null },
|
||||
onMessageClicked = navController::navigateToMessagesHistory,
|
||||
onNavigateToCreateChat = navController::navigateToCreateChat
|
||||
)
|
||||
@@ -144,11 +157,14 @@ fun RootScreen(
|
||||
messagesHistoryScreen(
|
||||
onError = viewModel::onError,
|
||||
onBack = navController::navigateUp,
|
||||
onNavigateToChatMaterials = navController::navigateToChatMaterials
|
||||
onNavigateToChatMaterials = navController::navigateToChatMaterials,
|
||||
onNavigateToPhotoViewer = { photos, index ->
|
||||
photoViewerInfo = photos to index
|
||||
}
|
||||
)
|
||||
chatMaterialsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
|
||||
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null }
|
||||
)
|
||||
createChatScreen(
|
||||
onChatCreated = { conversationId ->
|
||||
@@ -161,11 +177,23 @@ fun RootScreen(
|
||||
settingsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onLogOutButtonClicked = { navController.navigateToAuth(true) },
|
||||
onLanguageItemClicked = navController::navigateToLanguagePicker
|
||||
onLanguageItemClicked = navController::navigateToLanguagePicker,
|
||||
onRestartRequired = {
|
||||
activity?.let {
|
||||
val intent = Intent(activity, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
)
|
||||
languagePickerScreen(onBack = navController::navigateUp)
|
||||
}
|
||||
|
||||
photoViewScreen(onBack = navController::navigateUp)
|
||||
PhotoViewDialog(
|
||||
photoViewerInfo = photoViewerInfo,
|
||||
onDismiss = { photoViewerInfo = null }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import dev.meloda.fast.common.AppConstants
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
object NotificationsUtils {
|
||||
|
||||
@@ -28,7 +28,7 @@ object NotificationsUtils {
|
||||
actions: List<NotificationCompat.Action> = emptyList(),
|
||||
): NotificationCompat.Builder {
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(UiR.drawable.ic_fast_logo)
|
||||
.setSmallIcon(R.drawable.ic_fast_logo)
|
||||
.setContentTitle(title)
|
||||
.setPriority(priority.value)
|
||||
.setContentIntent(contentIntent)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Allow cleartext network traffic -->
|
||||
<base-config
|
||||
cleartextTrafficPermitted="true"
|
||||
cleartextTrafficPermitted="false"
|
||||
tools:ignore="InsecureBaseConfiguration">
|
||||
<trust-anchors>
|
||||
<!-- Trust pre-installed CAs -->
|
||||
|
||||
@@ -15,7 +15,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
|
||||
defaultConfig.targetSdk = 36
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||
|
||||
val extension = extensions.getByType<LibraryExtension>()
|
||||
extension.androidResources.enable = false
|
||||
configureAndroidCompose(extension)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import dev.meloda.fast.configureKotlinAndroid
|
||||
import dev.meloda.fast.disableUnnecessaryAndroidTests
|
||||
import dev.meloda.fast.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
@@ -21,7 +20,8 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
|
||||
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
|
||||
androidResources.enable = false
|
||||
defaultConfig.targetSdk = 36
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
extensions.configure<LibraryAndroidComponentsExtension> {
|
||||
|
||||
@@ -15,7 +15,7 @@ class AndroidTestConventionPlugin : Plugin<Project> {
|
||||
|
||||
extensions.configure<TestExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
|
||||
defaultConfig.targetSdk = 36
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ internal fun Project.configureKotlinAndroid(
|
||||
commonExtension: CommonExtension<*, *, *, *, *, *>,
|
||||
) {
|
||||
commonExtension.apply {
|
||||
compileSdk = libs.findVersion("compileSdk").get().toString().toInt()
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.findVersion("minSdk").get().toString().toInt()
|
||||
minSdk = 23
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
||||
@@ -5,8 +5,8 @@ object AppConstants {
|
||||
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
|
||||
|
||||
const val API_VERSION = "5.238"
|
||||
const val URL_OAUTH = "https://oauth.vk.com"
|
||||
const val URL_API = "https://api.vk.com/method"
|
||||
const val URL_OAUTH = "https://oauth.vk.ru"
|
||||
const val URL_API = "https://api.vk.ru/method"
|
||||
|
||||
const val NOTIFICATION_CHANNEL_UNCATEGORIZED = "uncategorized"
|
||||
const val NOTIFICATION_CHANNEL_LONG_POLLING = "long_polling"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dev.meloda.fast.common.extensions
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -11,6 +13,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -103,7 +106,7 @@ fun Any.asInt(): Int {
|
||||
}
|
||||
|
||||
fun Any.asLong(): Long {
|
||||
return when(this) {
|
||||
return when (this) {
|
||||
is Number -> this.toLong()
|
||||
|
||||
else -> throw IllegalArgumentException("Object is not numeric")
|
||||
@@ -117,3 +120,21 @@ fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Bundle.getParcelableCompat(key: String, clazz: Class<T>): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelable(key, clazz)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelable(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> Bundle.getParcelableCompat(key: String, clazz: KClass<T>): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelable(key, clazz.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelable(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package dev.meloda.fast.common.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import com.conena.nanokt.jvm.util.dayOfMonth
|
||||
import com.conena.nanokt.jvm.util.hour
|
||||
import com.conena.nanokt.jvm.util.hourOfDay
|
||||
@@ -9,7 +8,6 @@ import com.conena.nanokt.jvm.util.minute
|
||||
import com.conena.nanokt.jvm.util.month
|
||||
import com.conena.nanokt.jvm.util.second
|
||||
import com.conena.nanokt.jvm.util.year
|
||||
import dev.meloda.fast.common.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
@@ -27,7 +25,11 @@ object TimeUtils {
|
||||
}.timeInMillis
|
||||
}
|
||||
|
||||
fun getLocalizedDate(resources: Resources, date: Long): String {
|
||||
fun getLocalizedDate(
|
||||
date: Long,
|
||||
yesterday: () -> String,
|
||||
today: () -> String
|
||||
): String {
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
||||
|
||||
@@ -36,43 +38,50 @@ object TimeUtils {
|
||||
now.month != then.month -> "dd MMMM"
|
||||
now.dayOfMonth != then.dayOfMonth -> {
|
||||
if (now.dayOfMonth - then.dayOfMonth == 1) {
|
||||
return resources.getString(R.string.yesterday)
|
||||
return yesterday()
|
||||
} else {
|
||||
"dd MMMM"
|
||||
}
|
||||
}
|
||||
|
||||
else -> return resources.getString(R.string.today)
|
||||
else -> return today()
|
||||
}
|
||||
|
||||
return SimpleDateFormat(pattern, Locale.getDefault()).format(date)
|
||||
}
|
||||
|
||||
fun getLocalizedTime(resources: Resources, date: Long): String {
|
||||
fun getLocalizedTime(
|
||||
date: Long,
|
||||
yearShort: () -> String,
|
||||
monthShort: () -> String,
|
||||
weekShort: () -> String,
|
||||
dayShort: () -> String,
|
||||
now: () -> String
|
||||
): String {
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
||||
|
||||
return when {
|
||||
now.year != then.year -> {
|
||||
"${now.year - then.year}${resources.getString(R.string.year_short).lowercase()}"
|
||||
"${now.year - then.year}${yearShort().lowercase()}"
|
||||
}
|
||||
|
||||
now.month != then.month -> {
|
||||
"${now.month - then.month}${resources.getString(R.string.month_short).lowercase()}"
|
||||
"${now.month - then.month}${monthShort().lowercase()}"
|
||||
}
|
||||
|
||||
now.dayOfMonth != then.dayOfMonth -> {
|
||||
val change = now.dayOfMonth - then.dayOfMonth
|
||||
|
||||
if (change % 7 == 0) {
|
||||
"${change / 7}${resources.getString(R.string.week_short).lowercase()}"
|
||||
"${change / 7}${weekShort().lowercase()}"
|
||||
} else {
|
||||
"$change${resources.getString(R.string.day_short).lowercase()}"
|
||||
"$change${dayShort().lowercase()}"
|
||||
}
|
||||
}
|
||||
|
||||
now.hour == then.hour && now.minute == then.minute -> {
|
||||
resources.getString(R.string.time_now).lowercase()
|
||||
now().lowercase()
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.meloda.fast.common.util
|
||||
|
||||
import java.net.URLEncoder
|
||||
|
||||
fun String.urlEncode(encoding: String = "utf-8"): String {
|
||||
return URLEncoder.encode(this, encoding)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="yesterday">Вчера</string>
|
||||
<string name="today">Сегодня</string>
|
||||
<string name="year_short">Г</string>
|
||||
<string name="month_short">М</string>
|
||||
<string name="week_short">Н</string>
|
||||
<string name="day_short">Д</string>
|
||||
<string name="time_now">Сейчас</string>
|
||||
</resources>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="yesterday">Yesterday</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="year_short">Y</string>
|
||||
<string name="month_short">M</string>
|
||||
<string name="week_short">W</string>
|
||||
<string name="day_short">D</string>
|
||||
<string name="time_now">Now</string>
|
||||
</resources>
|
||||
@@ -36,7 +36,7 @@ class VkUsersMap(
|
||||
if (message.fromId > 0) map[message.fromId]
|
||||
else null
|
||||
|
||||
fun user(userid: Long): VkUser? = map[userId]
|
||||
fun user(userId: Long): VkUser? = map[userId]
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
+7
-1
@@ -75,7 +75,13 @@ class ConversationsRepositoryImpl(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message)
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
replyMessage = message.replyMessage?.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
)
|
||||
).also { VkMemoryCache[message.id] = it }
|
||||
}
|
||||
item.conversation.asDomain(lastMessage).let { conversation ->
|
||||
|
||||
@@ -6,6 +6,7 @@ import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||
import dev.meloda.fast.network.RestApiErrorDomain
|
||||
|
||||
@@ -32,7 +33,7 @@ interface MessagesRepository {
|
||||
peerId: Long,
|
||||
randomId: Long,
|
||||
message: String?,
|
||||
replyTo: Long?,
|
||||
forward: String?,
|
||||
attachments: List<VkAttachment>?,
|
||||
formatData: VkMessage.FormatData?
|
||||
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
|
||||
@@ -110,4 +111,9 @@ interface MessagesRepository {
|
||||
chatId: Long,
|
||||
memberId: Long
|
||||
): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
suspend fun getMessageReadPeers(
|
||||
peerId: Long,
|
||||
cmId: Long
|
||||
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain>
|
||||
}
|
||||
|
||||
+37
-5
@@ -37,6 +37,7 @@ import dev.meloda.fast.model.api.requests.MessagesRemoveChatUserRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesSendRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||
import dev.meloda.fast.network.RestApiErrorDomain
|
||||
import dev.meloda.fast.network.mapApiDefault
|
||||
@@ -90,7 +91,13 @@ class MessagesRepositoryImpl(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message)
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
replyMessage = message.replyMessage?.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
)
|
||||
).also { VkMemoryCache[message.id] = it }
|
||||
}
|
||||
}
|
||||
@@ -159,7 +166,13 @@ class MessagesRepositoryImpl(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message)
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
replyMessage = message.replyMessage?.asDomain()?.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -183,7 +196,7 @@ class MessagesRepositoryImpl(
|
||||
peerId: Long,
|
||||
randomId: Long,
|
||||
message: String?,
|
||||
replyTo: Long?,
|
||||
forward: String?,
|
||||
attachments: List<VkAttachment>?,
|
||||
formatData: VkMessage.FormatData?
|
||||
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
@@ -191,7 +204,7 @@ class MessagesRepositoryImpl(
|
||||
peerId = peerId,
|
||||
randomId = randomId,
|
||||
message = message,
|
||||
replyTo = replyTo,
|
||||
forward = forward,
|
||||
attachments = attachments,
|
||||
formatData = formatData
|
||||
)
|
||||
@@ -308,7 +321,12 @@ class MessagesRepositoryImpl(
|
||||
messagesIds = messageIds.orEmpty(),
|
||||
important = important
|
||||
)
|
||||
messagesService.markAsImportant(requestModel.map).mapApiDefault()
|
||||
messagesService.markAsImportant(requestModel.map).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
apiResponse.requireResponse().marked.map { it.cmId }
|
||||
},
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun delete(
|
||||
@@ -402,4 +420,18 @@ class MessagesRepositoryImpl(
|
||||
|
||||
messagesService.removeChatUser(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun getMessageReadPeers(
|
||||
peerId: Long,
|
||||
cmId: Long
|
||||
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
messagesService.getMessageReadPeers(
|
||||
mapOf(
|
||||
"peer_id" to peerId.toString(),
|
||||
"cmid" to cmId.toString(),
|
||||
"extended" to "1",
|
||||
"fields" to VkConstants.USER_FIELDS
|
||||
)
|
||||
).mapApiDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ val databaseModule = module {
|
||||
|
||||
single {
|
||||
Room.databaseBuilder(get(), CacheDatabase::class.java, "cache")
|
||||
.fallbackToDestructiveMigration()
|
||||
.fallbackToDestructiveMigration(true)
|
||||
.build()
|
||||
}
|
||||
single { cacheDB().userDao() }
|
||||
|
||||
@@ -96,6 +96,20 @@ object AppSettings {
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
|
||||
|
||||
var showAttachmentButton: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON,
|
||||
SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, value)
|
||||
|
||||
var showManualRefreshOptions: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS,
|
||||
SettingsKeys.DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS, value)
|
||||
|
||||
var enableHaptic: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_ENABLE_HAPTIC,
|
||||
|
||||
@@ -11,6 +11,10 @@ object SettingsKeys {
|
||||
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
|
||||
const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button"
|
||||
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
|
||||
const val KEY_SHOW_ATTACHMENT_BUTTON = "show_attachment_button"
|
||||
const val DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON = false
|
||||
const val KEY_SHOW_MANUAL_REFRESH_OPTIONS = "show_manual_refresh_options"
|
||||
const val DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS = false
|
||||
|
||||
const val KEY_APPEARANCE = "appearance"
|
||||
const val KEY_APPEARANCE_MULTILINE = "appearance_multiline"
|
||||
@@ -46,6 +50,9 @@ object SettingsKeys {
|
||||
const val DEFAULT_ENABLE_HAPTIC = true
|
||||
const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level"
|
||||
const val DEFAULT_NETWORK_LOG_LEVEL = 3
|
||||
const val KEY_DEBUG_IMPORT_AUTH_DATA = "debug_import_auth_data"
|
||||
const val KEY_DEBUG_EXPORT_AUTH_DATA = "debug_export_auth_data"
|
||||
|
||||
const val KEY_USE_SYSTEM_FONT = "use_system_font"
|
||||
const val DEFAULT_USE_SYSTEM_FONT = false
|
||||
const val KEY_MORE_ANIMATIONS = "more_animations"
|
||||
|
||||
@@ -14,15 +14,10 @@ interface UserSettings {
|
||||
val enableDynamicColors: StateFlow<Boolean>
|
||||
val appLanguage: StateFlow<String>
|
||||
|
||||
val fastText: StateFlow<String>
|
||||
|
||||
val sendOnlineStatus: StateFlow<Boolean>
|
||||
|
||||
val showAlertAfterCrash: StateFlow<Boolean>
|
||||
val longPollInBackground: StateFlow<Boolean>
|
||||
val useBlur: StateFlow<Boolean>
|
||||
val showEmojiButton: StateFlow<Boolean>
|
||||
val showTimeInActionMessages: StateFlow<Boolean>
|
||||
val useSystemFont: StateFlow<Boolean>
|
||||
val enableAnimations: StateFlow<Boolean>
|
||||
val showDebugCategory: StateFlow<Boolean>
|
||||
@@ -35,15 +30,10 @@ interface UserSettings {
|
||||
fun onEnableDynamicColorsChanged(enable: Boolean)
|
||||
fun onAppLanguageChanged(language: String)
|
||||
|
||||
fun onFastTextChanged(text: String)
|
||||
|
||||
fun onSendOnlineStatusChanged(send: Boolean)
|
||||
|
||||
fun onShowAlertAfterCrashChanged(show: Boolean)
|
||||
fun onLongPollInBackgroundChanged(inBackground: Boolean)
|
||||
fun onUseBlurChanged(use: Boolean)
|
||||
fun onShowEmojiButtonChanged(show: Boolean)
|
||||
fun onShowTimeInActionMessagesChanged(show: Boolean)
|
||||
fun onUseSystemFontChanged(use: Boolean)
|
||||
fun onShowDebugCategoryChanged(show: Boolean)
|
||||
}
|
||||
@@ -58,16 +48,11 @@ class UserSettingsImpl : UserSettings {
|
||||
override val enableDynamicColors = MutableStateFlow(AppSettings.Appearance.enableDynamicColors)
|
||||
override val appLanguage = MutableStateFlow(AppSettings.Appearance.appLanguage)
|
||||
|
||||
override val fastText = MutableStateFlow(AppSettings.Features.fastText)
|
||||
|
||||
override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus)
|
||||
|
||||
override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash)
|
||||
override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground)
|
||||
override val longPollInBackground =
|
||||
MutableStateFlow(AppSettings.Experimental.longPollInBackground)
|
||||
override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur)
|
||||
override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton)
|
||||
override val showTimeInActionMessages =
|
||||
MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages)
|
||||
override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont)
|
||||
override val enableAnimations = MutableStateFlow(AppSettings.Experimental.moreAnimations)
|
||||
override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory)
|
||||
@@ -96,18 +81,10 @@ class UserSettingsImpl : UserSettings {
|
||||
appLanguage.value = language
|
||||
}
|
||||
|
||||
override fun onFastTextChanged(text: String) {
|
||||
fastText.value = text
|
||||
}
|
||||
|
||||
override fun onSendOnlineStatusChanged(send: Boolean) {
|
||||
sendOnlineStatus.value = send
|
||||
}
|
||||
|
||||
override fun onShowAlertAfterCrashChanged(show: Boolean) {
|
||||
showAlertAfterCrash.value = show
|
||||
}
|
||||
|
||||
override fun onLongPollInBackgroundChanged(inBackground: Boolean) {
|
||||
longPollInBackground.value = inBackground
|
||||
}
|
||||
@@ -116,14 +93,6 @@ class UserSettingsImpl : UserSettings {
|
||||
useBlur.value = use
|
||||
}
|
||||
|
||||
override fun onShowEmojiButtonChanged(show: Boolean) {
|
||||
showEmojiButton.value = show
|
||||
}
|
||||
|
||||
override fun onShowTimeInActionMessagesChanged(show: Boolean) {
|
||||
showTimeInActionMessages.value = show
|
||||
}
|
||||
|
||||
override fun onUseSystemFontChanged(use: Boolean) {
|
||||
useSystemFont.value = use
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.meloda.fast.domain
|
||||
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.api.messages.MessagesRepository
|
||||
import dev.meloda.fast.data.mapToState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetMessageReadPeersUseCase(
|
||||
private val repository: MessagesRepository
|
||||
) : BaseUseCase {
|
||||
|
||||
operator fun invoke(
|
||||
peerId: Long,
|
||||
cmId: Long
|
||||
): Flow<State<Int>> = flowNewState {
|
||||
repository.getMessageReadPeers(
|
||||
peerId = peerId,
|
||||
cmId = cmId
|
||||
).mapToState(successMapper = { it.totalCount })
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ interface MessagesUseCase : BaseUseCase {
|
||||
peerId: Long,
|
||||
randomId: Long,
|
||||
message: String?,
|
||||
replyTo: Long?,
|
||||
forward: String?,
|
||||
attachments: List<VkAttachment>?,
|
||||
formatData: VkMessage.FormatData?
|
||||
): Flow<State<MessagesSendResponse>>
|
||||
|
||||
@@ -56,7 +56,7 @@ class MessagesUseCaseImpl(
|
||||
peerId: Long,
|
||||
randomId: Long,
|
||||
message: String?,
|
||||
replyTo: Long?,
|
||||
forward: String?,
|
||||
attachments: List<VkAttachment>?,
|
||||
formatData: VkMessage.FormatData?
|
||||
): Flow<State<MessagesSendResponse>> = flowNewState {
|
||||
@@ -64,7 +64,7 @@ class MessagesUseCaseImpl(
|
||||
peerId = peerId,
|
||||
randomId = randomId,
|
||||
message = message,
|
||||
replyTo = replyTo,
|
||||
forward = forward,
|
||||
attachments = attachments,
|
||||
formatData = formatData
|
||||
).mapToState()
|
||||
|
||||
@@ -6,6 +6,7 @@ import dev.meloda.fast.domain.AccountUseCaseImpl
|
||||
import dev.meloda.fast.domain.GetCurrentAccountUseCase
|
||||
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
|
||||
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
|
||||
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
||||
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
||||
@@ -27,4 +28,6 @@ val domainModule = module {
|
||||
singleOf(::GetCurrentAccountUseCase)
|
||||
|
||||
singleOf(::LoadConversationsByIdUseCase)
|
||||
|
||||
singleOf(::GetMessageReadPeersUseCase)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package dev.meloda.fast.model.api.data
|
||||
|
||||
import dev.meloda.fast.model.api.domain.VkGiftDomain
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import dev.meloda.fast.model.api.domain.VkGiftDomain
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VkGiftData(
|
||||
@Json(name = "id") val id: Long,
|
||||
@Json(name = "thumb_512") val thumb512: String?,
|
||||
@Json(name = "thumb_256") val thumb256: String?,
|
||||
@Json(name = "thumb_96") val thumb96: String?,
|
||||
@Json(name = "thumb_48") val thumb48: String
|
||||
@@ -14,6 +15,7 @@ data class VkGiftData(
|
||||
|
||||
fun toDomain() = VkGiftDomain(
|
||||
id = id,
|
||||
thumb512 = thumb512,
|
||||
thumb256 = thumb256,
|
||||
thumb96 = thumb96,
|
||||
thumb48 = thumb48
|
||||
|
||||
@@ -105,7 +105,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
|
||||
actionConversationMessageId = action?.conversationMessageId,
|
||||
actionMessage = action?.message,
|
||||
geoType = geo?.type,
|
||||
isImportant = important ?: false,
|
||||
isImportant = important == true,
|
||||
updateTime = updateTime,
|
||||
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
|
||||
attachments = attachments.map(VkAttachmentItemData::toDomain),
|
||||
|
||||
@@ -37,7 +37,7 @@ data class VkUserData(
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LastSeen(
|
||||
@Json(name = "platform") val platform: Int,
|
||||
@Json(name = "platform") val platform: Int?,
|
||||
@Json(name = "time") val time: Int
|
||||
)
|
||||
|
||||
|
||||
@@ -54,9 +54,9 @@ data class VkVideoMessageData(
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Image(
|
||||
val height: Int?,
|
||||
val url: String?,
|
||||
val width: Int?,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int,
|
||||
val with_padding: Int?,
|
||||
)
|
||||
|
||||
@@ -73,6 +73,7 @@ data class VkVideoMessageData(
|
||||
)
|
||||
|
||||
fun toDomain(): VkVideoMessageDomain = VkVideoMessageDomain(
|
||||
id = id
|
||||
id = id,
|
||||
image = image.orEmpty().filter { it.width / it.height == 1 }.maxByOrNull { it.width }?.url
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ import dev.meloda.fast.model.api.data.AttachmentType
|
||||
|
||||
data class VkGiftDomain(
|
||||
val id: Long,
|
||||
val thumb512: String?,
|
||||
val thumb256: String?,
|
||||
val thumb96: String?,
|
||||
val thumb48: String
|
||||
) : VkAttachment {
|
||||
|
||||
override val type: AttachmentType = AttachmentType.GIFT
|
||||
|
||||
fun getMaxSizeThumb(): String = thumb512 ?: thumb256 ?: thumb96 ?: thumb48
|
||||
fun getDefaultThumbSizeOrLess(): String = thumb256 ?: thumb96 ?: thumb48
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ data class VkMessage(
|
||||
val user: VkUser?,
|
||||
val group: VkGroupDomain?,
|
||||
val actionUser: VkUser?,
|
||||
val actionGroup: VkGroupDomain?
|
||||
val actionGroup: VkGroupDomain?,
|
||||
) {
|
||||
|
||||
fun isPeerChat() = peerId > 2_000_000_000
|
||||
|
||||
@@ -21,4 +21,12 @@ data class VkStickerDomain(
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun getUrl(width: Int = 256, withBackground: Boolean = false): String? = when {
|
||||
withBackground && backgroundImages != null -> {
|
||||
backgroundImages.firstOrNull { it.width >= width }?.url
|
||||
}
|
||||
images != null -> images.firstOrNull { it.width >= width }?.url
|
||||
else -> "https://vk.ru/sticker/1-${id}-${width}b"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package dev.meloda.fast.model.api.domain
|
||||
import dev.meloda.fast.model.api.data.AttachmentType
|
||||
|
||||
data class VkVideoMessageDomain(
|
||||
val id: Long
|
||||
val id: Long,
|
||||
val image: String?
|
||||
) : VkAttachment {
|
||||
|
||||
override val type: AttachmentType = AttachmentType.VIDEO_MESSAGE
|
||||
|
||||
@@ -34,7 +34,7 @@ data class MessagesSendRequest(
|
||||
val message: String?,
|
||||
val lat: Int? = null,
|
||||
val lon: Int? = null,
|
||||
val replyTo: Long? = null,
|
||||
val forward: String? = null,
|
||||
val stickerId: Long? = null,
|
||||
val disableMentions: Boolean? = null,
|
||||
val doNotParseLinks: Boolean? = null,
|
||||
@@ -51,7 +51,7 @@ data class MessagesSendRequest(
|
||||
message?.let { this["message"] = it }
|
||||
lat?.let { this["lat"] = it.toString() }
|
||||
lon?.let { this["lon"] = it.toString() }
|
||||
replyTo?.let { this["reply_to"] = it.toString() }
|
||||
forward?.let { this["forward"] = it }
|
||||
stickerId?.let { this["sticker_id"] = it.toString() }
|
||||
disableMentions?.let { this["disable_mentions"] = it.asInt().toString() }
|
||||
doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() }
|
||||
|
||||
@@ -35,7 +35,7 @@ data class AuthDirectRequest(
|
||||
}
|
||||
|
||||
data class AuthWithAppRequest(
|
||||
val redirectUrl: String = "https://oauth.vk.com/blank.html",
|
||||
val redirectUrl: String = "https://oauth.vk.ru/blank.html",
|
||||
val display: String = "page",
|
||||
val responseType: String = "token",
|
||||
val accessToken: String,
|
||||
|
||||
@@ -58,3 +58,22 @@ data class MessagesSendResponse(
|
||||
@Json(name = "message_id") val messageId: Long,
|
||||
@Json(name = "cmid") val cmId: Long
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagesMarkAsImportantResponse(
|
||||
@Json(name = "marked") val marked: List<Mark>
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Mark(
|
||||
@Json(name = "cmid") val cmId: Long,
|
||||
@Json(name = "message_id") val messageId: Long,
|
||||
@Json(name = "peer_id") val peerId: Long
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagesGetReadPeersResponse(
|
||||
@Json(name = "items") val items: List<Long>,
|
||||
@Json(name = "total_count") val totalCount: Int,
|
||||
@Json(name = "profiles") val profiles: List<VkUserData>?,
|
||||
)
|
||||
|
||||
+9
-1
@@ -9,6 +9,8 @@ import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesMarkAsImportantResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||
import dev.meloda.fast.network.ApiResponse
|
||||
import dev.meloda.fast.network.RestApiError
|
||||
@@ -76,7 +78,7 @@ interface MessagesService {
|
||||
@POST(MessagesUrls.MARK_AS_IMPORTANT)
|
||||
suspend fun markAsImportant(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<List<Long>>, RestApiError>
|
||||
): ApiResult<ApiResponse<MessagesMarkAsImportantResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(MessagesUrls.DELETE)
|
||||
@@ -107,4 +109,10 @@ interface MessagesService {
|
||||
suspend fun removeChatUser(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(MessagesUrls.GET_MESSAGE_READ_PEERS)
|
||||
suspend fun getMessageReadPeers(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<MessagesGetReadPeersResponse>, RestApiError>
|
||||
}
|
||||
|
||||
@@ -22,4 +22,5 @@ object MessagesUrls {
|
||||
const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser"
|
||||
const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments"
|
||||
const val CREATE_CHAT = "$URL/messages.createChat"
|
||||
const val GET_MESSAGE_READ_PEERS = "$URL/messages.getMessageReadPeers"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "dev.meloda.fast.ui"
|
||||
androidResources.enable = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -19,6 +20,8 @@ dependencies {
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
|
||||
implementation(libs.bundles.nanokt)
|
||||
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlin.serialization)
|
||||
implementation(libs.koin.androidx.compose.navigation)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.meloda.fast.ui.common
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import dev.meloda.fast.ui.model.DeviceSize
|
||||
import dev.meloda.fast.ui.model.SizeConfig
|
||||
|
||||
val LocalSizeConfig = compositionLocalOf {
|
||||
SizeConfig(
|
||||
widthSize = DeviceSize.Compact,
|
||||
heightSize = DeviceSize.Compact
|
||||
)
|
||||
}
|
||||
@@ -20,12 +20,12 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
@Composable
|
||||
fun ErrorView(
|
||||
modifier: Modifier = Modifier,
|
||||
iconResId: Int? = UiR.drawable.round_error_24,
|
||||
iconResId: Int? = R.drawable.round_error_24,
|
||||
text: String,
|
||||
buttonText: String? = null,
|
||||
onButtonClick: (() -> Unit)? = null,
|
||||
|
||||
+4
-26
@@ -1,19 +1,16 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Indication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalUseFallbackRippleImplementation
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,12 +20,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun IconButton(
|
||||
fun FastIconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
@@ -49,10 +45,7 @@ fun IconButton(
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
indication = rippleOrFallbackImplementation(
|
||||
bounded = false,
|
||||
radius = IconButtonTokens.StateLayerSize / 2
|
||||
)
|
||||
indication = ripple()
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -61,21 +54,6 @@ fun IconButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun rippleOrFallbackImplementation(
|
||||
bounded: Boolean = true,
|
||||
radius: Dp = Dp.Unspecified,
|
||||
color: Color = Color.Unspecified
|
||||
): Indication {
|
||||
return if (LocalUseFallbackRippleImplementation.current) {
|
||||
rememberRipple(bounded, radius, color)
|
||||
} else {
|
||||
ripple(bounded, radius, color)
|
||||
}
|
||||
}
|
||||
|
||||
internal object IconButtonTokens {
|
||||
val StateLayerShape = CircleShape
|
||||
val StateLayerSize = 40.0.dp
|
||||
@@ -0,0 +1,109 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FastTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
prefix: @Composable (() -> Unit)? = null,
|
||||
suffix: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||
// If color is not provided via the text style, use content color as a default
|
||||
val textColor =
|
||||
textStyle.color.takeOrElse {
|
||||
val focused = interactionSource.collectIsFocusedAsState().value
|
||||
colors.textColor(enabled, isError, focused)
|
||||
}
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier =
|
||||
modifier,
|
||||
/* .defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight,
|
||||
)*/
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
cursorBrush = SolidColor(colors.cursorColor(isError)),
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
decorationBox =
|
||||
@Composable { innerTextField ->
|
||||
// places leading icon, text field with label and placeholder, trailing icon
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = value.text,
|
||||
visualTransformation = visualTransformation,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = enabled,
|
||||
isError = isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.zIndex
|
||||
|
||||
@Composable
|
||||
fun FullScreenDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismiss: () -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = false,
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(10F),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
fun FullScreenLoader(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FullScreenLoaderPreview() {
|
||||
FullScreenLoader()
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.ContainedLoadingIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun FullScreenContainedLoader(
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color = MaterialTheme.colorScheme.primary,
|
||||
indicatorColor: Color = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ContainedLoader(
|
||||
containerColor = containerColor,
|
||||
indicatorColor = indicatorColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun FullScreenLoader(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Loader(color = color)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun ContainedLoader(
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color = MaterialTheme.colorScheme.primary,
|
||||
indicatorColor: Color = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
ContainedLoadingIndicator(
|
||||
modifier = modifier,
|
||||
containerColor = containerColor,
|
||||
indicatorColor = indicatorColor
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun Loader(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
LoadingIndicator(
|
||||
modifier = modifier,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun RippledClickContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(4.dp),
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.combinedClickable(
|
||||
interactionSource = null,
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package dev.meloda.fast.ui.extensions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
|
||||
@Composable
|
||||
fun <T> ProvidableCompositionLocal<T?>.getOrThrow(): T {
|
||||
return requireNotNull(current)
|
||||
}
|
||||
@@ -4,112 +4,27 @@ import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.navigation.NavController
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.model.DeviceSize
|
||||
import dev.meloda.fast.ui.model.SizeConfig
|
||||
import dev.meloda.fast.ui.model.ThemeConfig
|
||||
|
||||
private val googleSansFonts = FontFamily(
|
||||
Font(resId = R.font.google_sans_regular),
|
||||
Font(
|
||||
resId = R.font.google_sans_italic,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_medium,
|
||||
weight = FontWeight.Medium
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_medium_italic,
|
||||
weight = FontWeight.Medium,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_bold,
|
||||
weight = FontWeight.Bold
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_bold_italic,
|
||||
weight = FontWeight.Bold,
|
||||
style = FontStyle.Italic
|
||||
)
|
||||
)
|
||||
|
||||
private val robotoFonts = FontFamily(
|
||||
Font(
|
||||
resId = R.font.roboto_thin,
|
||||
weight = FontWeight.Thin
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_thin_italic,
|
||||
weight = FontWeight.Thin,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_light,
|
||||
weight = FontWeight.Light
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_light_italic,
|
||||
weight = FontWeight.Light,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(resId = R.font.roboto_regular),
|
||||
Font(
|
||||
resId = R.font.roboto_italic,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_medium,
|
||||
weight = FontWeight.Medium
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_medium_italic,
|
||||
weight = FontWeight.Medium,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_bold,
|
||||
weight = FontWeight.Bold
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_bold_italic,
|
||||
weight = FontWeight.Bold,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_black,
|
||||
weight = FontWeight.Black
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_black_italic,
|
||||
weight = FontWeight.Black,
|
||||
style = FontStyle.Italic
|
||||
)
|
||||
)
|
||||
|
||||
val LocalThemeConfig = compositionLocalOf {
|
||||
ThemeConfig(
|
||||
darkMode = false,
|
||||
@@ -123,25 +38,14 @@ val LocalThemeConfig = compositionLocalOf {
|
||||
)
|
||||
}
|
||||
|
||||
val LocalSizeConfig = compositionLocalOf {
|
||||
SizeConfig(
|
||||
widthSize = DeviceSize.Compact,
|
||||
heightSize = DeviceSize.Compact
|
||||
)
|
||||
}
|
||||
|
||||
val LocalHazeState = compositionLocalOf { HazeState() }
|
||||
val LocalHazeState = compositionLocalOf { HazeState(true) }
|
||||
val LocalBottomPadding = compositionLocalOf { 0.dp }
|
||||
val LocalUser = compositionLocalOf<VkUser?> { null }
|
||||
val LocalReselectedTab = compositionLocalOf { mapOf<Any, Boolean>() }
|
||||
val LocalNavRootController = compositionLocalOf<NavController?> { null }
|
||||
val LocalNavController = compositionLocalOf<NavController?> { null }
|
||||
|
||||
@Composable
|
||||
fun <T: NavController> ProvidableCompositionLocal<T?>.getOrThrow(): T {
|
||||
return requireNotNull(current)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
predefinedColorScheme: ColorScheme? = null,
|
||||
@@ -186,21 +90,21 @@ fun AppTheme(
|
||||
MaterialTheme.typography
|
||||
} else {
|
||||
MaterialTheme.typography.copy(
|
||||
displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = googleSansFonts),
|
||||
displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = googleSansFonts),
|
||||
displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = googleSansFonts),
|
||||
headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = googleSansFonts),
|
||||
headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = googleSansFonts),
|
||||
headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = googleSansFonts),
|
||||
titleLarge = MaterialTheme.typography.titleLarge.copy(fontFamily = robotoFonts),
|
||||
titleMedium = MaterialTheme.typography.titleMedium.copy(fontFamily = robotoFonts),
|
||||
titleSmall = MaterialTheme.typography.titleSmall.copy(fontFamily = robotoFonts),
|
||||
bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = robotoFonts),
|
||||
bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = robotoFonts),
|
||||
bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = robotoFonts),
|
||||
labelLarge = MaterialTheme.typography.labelLarge.copy(fontFamily = robotoFonts),
|
||||
labelMedium = MaterialTheme.typography.labelMedium.copy(fontFamily = robotoFonts),
|
||||
labelSmall = MaterialTheme.typography.labelSmall.copy(fontFamily = robotoFonts),
|
||||
displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = GoogleSansFamily),
|
||||
displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = GoogleSansFamily),
|
||||
displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = GoogleSansFamily),
|
||||
headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = GoogleSansFamily),
|
||||
headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = GoogleSansFamily),
|
||||
headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = GoogleSansFamily),
|
||||
titleLarge = MaterialTheme.typography.titleLarge.copy(fontFamily = RobotoFamily),
|
||||
titleMedium = MaterialTheme.typography.titleMedium.copy(fontFamily = RobotoFamily),
|
||||
titleSmall = MaterialTheme.typography.titleSmall.copy(fontFamily = RobotoFamily),
|
||||
bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = RobotoFamily),
|
||||
bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = RobotoFamily),
|
||||
bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = RobotoFamily),
|
||||
labelLarge = MaterialTheme.typography.labelLarge.copy(fontFamily = RobotoFamily),
|
||||
labelMedium = MaterialTheme.typography.labelMedium.copy(fontFamily = RobotoFamily),
|
||||
labelSmall = MaterialTheme.typography.labelSmall.copy(fontFamily = RobotoFamily),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -213,7 +117,7 @@ fun AppTheme(
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = (predefinedColorScheme ?: colorScheme)
|
||||
.copy(
|
||||
primary = colorPrimary,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package dev.meloda.fast.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
val GoogleSansFamily = FontFamily(
|
||||
Font(resId = R.font.google_sans_regular),
|
||||
Font(
|
||||
resId = R.font.google_sans_italic,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_medium,
|
||||
weight = FontWeight.Medium
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_medium_italic,
|
||||
weight = FontWeight.Medium,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_bold,
|
||||
weight = FontWeight.Bold
|
||||
),
|
||||
Font(
|
||||
resId = R.font.google_sans_bold_italic,
|
||||
weight = FontWeight.Bold,
|
||||
style = FontStyle.Italic
|
||||
)
|
||||
)
|
||||
|
||||
val RobotoFamily = FontFamily(
|
||||
Font(
|
||||
resId = R.font.roboto_thin,
|
||||
weight = FontWeight.Thin
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_thin_italic,
|
||||
weight = FontWeight.Thin,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_light,
|
||||
weight = FontWeight.Light
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_light_italic,
|
||||
weight = FontWeight.Light,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(resId = R.font.roboto_regular),
|
||||
Font(
|
||||
resId = R.font.roboto_italic,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_medium,
|
||||
weight = FontWeight.Medium
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_medium_italic,
|
||||
weight = FontWeight.Medium,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_bold,
|
||||
weight = FontWeight.Bold
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_bold_italic,
|
||||
weight = FontWeight.Bold,
|
||||
style = FontStyle.Italic
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_black,
|
||||
weight = FontWeight.Black
|
||||
),
|
||||
Font(
|
||||
resId = R.font.roboto_black_italic,
|
||||
weight = FontWeight.Black,
|
||||
style = FontStyle.Italic
|
||||
)
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42 -0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M313,520L509,716Q521,728 520.5,744Q520,760 508,772Q496,783 480,783.5Q464,784 452,772L188,508Q182,502 179.5,495Q177,488 177,480Q177,472 179.5,465Q182,458 188,452L452,188Q463,177 479.5,177Q496,177 508,188Q520,200 520,216.5Q520,233 508,245L313,440L760,440Q777,440 788.5,451.5Q800,463 800,480Q800,497 788.5,508.5Q777,520 760,520L313,520Z" />
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M382,606L721,267Q733,255 749,255Q765,255 777,267Q789,279 789,295.5Q789,312 777,324L410,692Q398,704 382,704Q366,704 354,692L182,520Q170,508 170.5,491.5Q171,475 183,463Q195,451 211.5,451Q228,451 240,463L382,606Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M480,536L284,732Q273,743 256,743Q239,743 228,732Q217,721 217,704Q217,687 228,676L424,480L228,284Q217,273 217,256Q217,239 228,228Q239,217 256,217Q273,217 284,228L480,424L676,228Q687,217 704,217Q721,217 732,228Q743,239 743,256Q743,273 732,284L536,480L732,676Q743,687 743,704Q743,721 732,732Q721,743 704,743Q687,743 676,732L480,536Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M200,760L257,760L648,369L591,312L200,703L200,760ZM160,840Q143,840 131.5,828.5Q120,817 120,800L120,703Q120,687 126,672.5Q132,658 143,647L648,143Q660,132 674.5,126Q689,120 705,120Q721,120 736,126Q751,132 762,144L817,200Q829,211 834.5,226Q840,241 840,256Q840,272 834.5,286.5Q829,301 817,313L313,817Q302,828 287.5,834Q273,840 257,840L160,840ZM760,256L760,256L704,200L704,200L760,256ZM619,341L591,312L591,312L648,369L648,369L619,341Z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M480,432L324,588Q313,599 296,599Q279,599 268,588Q257,577 257,560Q257,543 268,532L452,348Q464,336 480,336Q496,336 508,348L692,532Q703,543 703,560Q703,577 692,588Q681,599 664,599Q647,599 636,588L480,432Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM800,320L501,507Q496,510 490.5,511.5Q485,513 480,513Q475,513 469.5,511.5Q464,510 459,507L160,320L160,720Q160,720 160,720Q160,720 160,720L800,720Q800,720 800,720Q800,720 800,720L800,320ZM480,440L800,240L160,240L480,440ZM160,320L160,330Q160,325 160,317.5Q160,310 160,301Q160,281 160,271Q160,261 160,272L160,240L160,240L160,272Q160,261 160,271.5Q160,282 160,301Q160,311 160,318.5Q160,326 160,330L160,320L160,720Q160,720 160,720Q160,720 160,720L160,720Q160,720 160,720Q160,720 160,720L160,320Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M8,19c1.1,0 2,-0.9 2,-2L10,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2v10c0,1.1 0.9,2 2,2zM14,7v10c0,1.1 0.9,2 2,2s2,-0.9 2,-2L18,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M320,687L320,273Q320,256 332,244.5Q344,233 360,233Q365,233 370.5,234.5Q376,236 381,239L707,446Q716,452 720.5,461Q725,470 725,480Q725,490 720.5,499Q716,508 707,514L381,721Q376,724 370.5,725.5Q365,727 360,727Q344,727 332,715.5Q320,704 320,687ZM400,480L400,480L400,480ZM400,614L610,480L400,346L400,614Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M480,800Q346,800 253,707Q160,614 160,480Q160,346 253,253Q346,160 480,160Q549,160 612,188.5Q675,217 720,270L720,200Q720,183 731.5,171.5Q743,160 760,160Q777,160 788.5,171.5Q800,183 800,200L800,400Q800,417 788.5,428.5Q777,440 760,440L560,440Q543,440 531.5,428.5Q520,417 520,400Q520,383 531.5,371.5Q543,360 560,360L688,360Q656,304 600.5,272Q545,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720Q548,720 604.5,685.5Q661,651 692,593Q700,579 714.5,573.5Q729,568 744,573Q760,578 767,594Q774,610 766,624Q725,704 649,752Q573,800 480,800Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M760,760L760,600Q760,550 725,515Q690,480 640,480L273,480L417,624L360,680L120,440L360,200L417,256L273,400L640,400Q723,400 781.5,458.5Q840,517 840,600L840,760L760,760Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M433,880Q406,880 386.5,862Q367,844 363,818L354,752Q341,747 329.5,740Q318,733 307,725L245,751Q220,762 195,753Q170,744 156,721L109,639Q95,616 101,590Q107,564 128,547L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L128,413Q107,396 101,370Q95,344 109,321L156,239Q170,216 195,207Q220,198 245,209L307,235Q318,227 330,220Q342,213 354,208L363,142Q367,116 386.5,98Q406,80 433,80L527,80Q554,80 573.5,98Q593,116 597,142L606,208Q619,213 630.5,220Q642,227 653,235L715,209Q740,198 765,207Q790,216 804,239L851,321Q865,344 859,370Q853,396 832,413L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L831,547Q852,564 858,590Q864,616 850,639L802,721Q788,744 763,753Q738,762 713,751L653,725Q642,733 630,740Q618,747 606,752L597,818Q593,844 573.5,862Q554,880 527,880L433,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4C7,4 2.73,7.11 1,11.5 2.73,15.89 7,19 12,19s9.27,-3.11 11,-7.5C21.27,7.11 17,4 12,4zM12,16.5c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,8.5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,700Q555,700 607.5,647.5Q660,595 660,520Q660,445 607.5,392.5Q555,340 480,340Q405,340 352.5,392.5Q300,445 300,520Q300,595 352.5,647.5Q405,700 480,700ZM480,620Q438,620 409,591Q380,562 380,520Q380,478 409,449Q438,420 480,420Q522,420 551,449Q580,478 580,520Q580,562 551,591Q522,620 480,620ZM160,840Q127,840 103.5,816.5Q80,793 80,760L80,280Q80,247 103.5,223.5Q127,200 160,200L286,200L336,146Q347,134 362.5,127Q378,120 395,120L565,120Q582,120 597.5,127Q613,134 624,146L674,200L800,200Q833,200 856.5,223.5Q880,247 880,280L880,760Q880,793 856.5,816.5Q833,840 800,840L160,840ZM160,760L800,760Q800,760 800,760Q800,760 800,760L800,280Q800,280 800,280Q800,280 800,280L638,280L565,200L395,200L322,280L160,280Q160,280 160,280Q160,280 160,280L160,760Q160,760 160,760Q160,760 160,760ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520L480,520L480,520L480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Z" />
|
||||
|
||||
</vector>
|
||||
@@ -194,6 +194,7 @@
|
||||
<string name="settings_general_title">Основное</string>
|
||||
<string name="settings_general_contact_names_title">Использовать имена контактов</string>
|
||||
<string name="settings_general_contact_names_summary">Приложение будет использовать доступные имена контактов для пользователей</string>
|
||||
<string name="settings_general_show_attachment_button_summary">Показывать кнопку вложений на панели чата</string>
|
||||
<string name="settings_general_enable_haptic_title">Включить тактильную отдачу</string>
|
||||
<string name="settings_appearance_title">Внешний вид</string>
|
||||
<string name="settings_appearance_multiline_title">Многострочные заголовки и сообщения</string>
|
||||
@@ -222,7 +223,7 @@
|
||||
<string name="settings_general_show_emoji_button_summary">Показывать кнопку эмоджи на панели чата</string>
|
||||
<string name="settings_features_show_time_in_action_messages_title">Показывать время в сервисных сообщениях</string>
|
||||
<string name="settings_experimental_use_blur_title">Использовать размытие</string>
|
||||
<string name="settings_experimental_use_blur_summary">Добавлять размытие везде, где возможно.\\nРаботает только с 12 версии Android</string>
|
||||
<string name="settings_experimental_use_blur_summary">Добавлять размытие везде, где возможно</string>
|
||||
<string name="settings_experimental_more_animations_title">Больше анимаций</string>
|
||||
<string name="warning_confirmation">Подтверждение</string>
|
||||
<string name="captcha_exit_warning">Вы уверены? Процесс ввода капчи будет отменён</string>
|
||||
@@ -269,4 +270,23 @@
|
||||
<string name="italic">Курсив</string>
|
||||
<string name="underline">Подчёркнутый</string>
|
||||
<string name="link">Ссылка</string>
|
||||
<string name="regular">Обычный</string>
|
||||
<string name="login_sign_up">Регистрация</string>
|
||||
<string name="login_forgot_password">Забыли пароль?</string>
|
||||
<string name="settings_general_show_attachment_button_title">Показывать кнопку вложений</string>
|
||||
<string name="action_copy_link">Скопировать ссылку</string>
|
||||
<string name="action_copy">Скопировать</string>
|
||||
<string name="action_copy_image">Скопировать изображение</string>
|
||||
<string name="action_open_in">Открыть в…</string>
|
||||
<string name="action_share">Поделиться</string>
|
||||
<string name="yesterday">Вчера</string>
|
||||
<string name="today">Сегодня</string>
|
||||
<string name="year_short">Г</string>
|
||||
<string name="month_short">М</string>
|
||||
<string name="week_short">Н</string>
|
||||
<string name="day_short">Д</string>
|
||||
<string name="time_now">Сейчас</string>
|
||||
|
||||
<string name="confirm_chat_create_with_title">Вы действительно хотите создать чат «%s»?</string>
|
||||
<string name="confirm_chat_create_empty_with_title">Вы действительно хотите создать чат «%s» только с собой?</string>
|
||||
</resources>
|
||||
|
||||
@@ -260,6 +260,8 @@
|
||||
<string name="settings_general_contact_names_summary">App will use available contact names for users</string>
|
||||
<string name="settings_general_show_emoji_button_title">Show emoji button</string>
|
||||
<string name="settings_general_show_emoji_button_summary">Show emoji button in chat panel</string>
|
||||
<string name="settings_general_show_attachment_button_title">Show attachment button</string>
|
||||
<string name="settings_general_show_attachment_button_summary">Show attachment button in chat panel</string>
|
||||
<string name="settings_general_enable_haptic_title">Enable haptic</string>
|
||||
<string name="settings_appearance_title">Appearance</string>
|
||||
<string name="settings_appearance_multiline_title">Multiline titles and messages</string>
|
||||
@@ -270,7 +272,7 @@
|
||||
<string name="settings_features_long_poll_in_background_summary">Your messages will be updating even when app is not on the screen</string>
|
||||
<string name="settings_features_show_time_in_action_messages_title">Show time in action messages</string>
|
||||
<string name="settings_experimental_use_blur_title">Use blur</string>
|
||||
<string name="settings_experimental_use_blur_summary">Adds blur wherever possible.\nWorks on android 12 and newer</string>
|
||||
<string name="settings_experimental_use_blur_summary">Adds blur wherever possible</string>
|
||||
<string name="settings_experimental_more_animations_title">More animations</string>
|
||||
<string name="settings_experimental_more_animations_summary">Use animations wherever possible</string>
|
||||
|
||||
@@ -340,9 +342,28 @@
|
||||
<string name="unspam_message_text">Are you sure you want to unmark this message as spam?</string>
|
||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||
|
||||
<string name="autofill">Autofill</string>
|
||||
<string name="autofill" tools:ignore="PrivateResource">Autofill</string>
|
||||
<string name="bold">Bold</string>
|
||||
<string name="italic">Italic</string>
|
||||
<string name="underline">Underline</string>
|
||||
<string name="link">Link</string>
|
||||
<string name="regular">Regular</string>
|
||||
<string name="login_sign_up">Sign up</string>
|
||||
<string name="login_forgot_password">Forgot password?</string>
|
||||
|
||||
<string name="action_copy_link">Copy link</string>
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="action_copy_image">Copy image</string>
|
||||
<string name="action_open_in">Open in…</string>
|
||||
<string name="action_share">Share</string>
|
||||
<string name="yesterday">Yesterday</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="year_short">Y</string>
|
||||
<string name="month_short">M</string>
|
||||
<string name="week_short">W</string>
|
||||
<string name="day_short">D</string>
|
||||
<string name="time_now">Now</string>
|
||||
|
||||
<string name="confirm_chat_create_with_title">Are you sure you want to create chat «%s»?</string>
|
||||
<string name="confirm_chat_create_empty_with_title">Are you sure you want to create chat «%s» only with yourself?</string>
|
||||
</resources>
|
||||
|
||||
@@ -23,6 +23,7 @@ object AuthGraph
|
||||
|
||||
fun NavGraphBuilder.authNavGraph(
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
navigation<AuthGraph>(startDestination = Login) {
|
||||
@@ -54,6 +55,7 @@ fun NavGraphBuilder.authNavGraph(
|
||||
)
|
||||
)
|
||||
},
|
||||
onNavigateToSettings = onNavigateToSettings,
|
||||
navController = navController
|
||||
)
|
||||
|
||||
|
||||
+11
-12
@@ -19,9 +19,6 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -50,15 +47,16 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import coil.compose.AsyncImage
|
||||
import dev.meloda.fast.auth.captcha.CaptchaViewModel
|
||||
import dev.meloda.fast.auth.captcha.CaptchaViewModelImpl
|
||||
import dev.meloda.fast.auth.captcha.model.CaptchaScreenState
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun CaptchaRoute(
|
||||
@@ -66,6 +64,7 @@ fun CaptchaRoute(
|
||||
onResult: (String) -> Unit,
|
||||
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
||||
) {
|
||||
LocalViewModelStoreOwner.current
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -116,11 +115,11 @@ fun CaptchaScreen(
|
||||
if (showExitAlert) {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { showExitAlert = false },
|
||||
title = stringResource(id = UiR.string.warning_confirmation),
|
||||
text = stringResource(id = UiR.string.captcha_exit_warning),
|
||||
title = stringResource(id = R.string.warning_confirmation),
|
||||
text = stringResource(id = R.string.captcha_exit_warning),
|
||||
confirmAction = { confirmedExit = true },
|
||||
confirmText = stringResource(id = UiR.string.yes),
|
||||
cancelText = stringResource(id = UiR.string.no),
|
||||
confirmText = stringResource(id = R.string.yes),
|
||||
cancelText = stringResource(id = R.string.no),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
)
|
||||
}
|
||||
@@ -147,7 +146,7 @@ fun CaptchaScreen(
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
painter = painterResource(R.drawable.round_close_24px),
|
||||
contentDescription = "Close icon",
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
@@ -187,7 +186,7 @@ fun CaptchaScreen(
|
||||
|
||||
if (LocalView.current.isInEditMode) {
|
||||
Image(
|
||||
painter = painterResource(id = UiR.drawable.test_captcha),
|
||||
painter = painterResource(id = R.drawable.test_captcha),
|
||||
contentDescription = "Captcha image",
|
||||
modifier = imageModifier
|
||||
)
|
||||
@@ -219,7 +218,7 @@ fun CaptchaScreen(
|
||||
.clip(RoundedCornerShape(10.dp)),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.round_qr_code_24),
|
||||
painter = painterResource(id = R.drawable.round_qr_code_24),
|
||||
contentDescription = "QR code icon",
|
||||
tint = if (showError) {
|
||||
MaterialTheme.colorScheme.error
|
||||
@@ -250,7 +249,7 @@ fun CaptchaScreen(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Done,
|
||||
painter = painterResource(R.drawable.round_check_24px),
|
||||
contentDescription = "Done icon",
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.auth.login
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -24,6 +25,7 @@ import dev.meloda.fast.data.db.AccountsRepository
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.data.success
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||
import dev.meloda.fast.domain.OAuthUseCase
|
||||
import dev.meloda.fast.model.database.AccountEntity
|
||||
@@ -33,65 +35,44 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface LoginViewModel {
|
||||
val screenState: StateFlow<LoginScreenState>
|
||||
val loginDialog: StateFlow<LoginDialog?>
|
||||
|
||||
val validationArguments: StateFlow<LoginValidationArguments?>
|
||||
val captchaArguments: StateFlow<CaptchaArguments?>
|
||||
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
||||
val isNeedToOpenMain: StateFlow<Boolean>
|
||||
|
||||
val isNeedToClearCaptchaCode: StateFlow<Boolean>
|
||||
val isNeedToClearValidationCode: StateFlow<Boolean>
|
||||
|
||||
fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle)
|
||||
fun onDialogDismissed(dialog: LoginDialog)
|
||||
|
||||
fun onBackPressed()
|
||||
|
||||
fun onPasswordVisibilityButtonClicked()
|
||||
|
||||
fun onLoginInputChanged(newLogin: String)
|
||||
fun onPasswordInputChanged(newPassword: String)
|
||||
|
||||
fun onSignInButtonClicked()
|
||||
|
||||
fun onNavigatedToMain()
|
||||
fun onNavigatedToUserBanned()
|
||||
fun onNavigatedToCaptcha()
|
||||
fun onNavigatedToValidation()
|
||||
|
||||
fun onValidationCodeReceived(code: String?)
|
||||
fun onValidationCodeCleared()
|
||||
fun onCaptchaCodeReceived(code: String?)
|
||||
fun onCaptchaCodeCleared()
|
||||
}
|
||||
|
||||
class LoginViewModelImpl(
|
||||
class LoginViewModel(
|
||||
private val oAuthUseCase: OAuthUseCase,
|
||||
private val authRepository: AuthRepository,
|
||||
private val loadUserByIdUseCase: LoadUserByIdUseCase,
|
||||
private val accountsRepository: AccountsRepository,
|
||||
private val loginValidator: LoginValidator,
|
||||
private val longPollController: LongPollController
|
||||
) : ViewModel(), LoginViewModel {
|
||||
private val longPollController: LongPollController,
|
||||
private val userSettings: UserSettings
|
||||
) : ViewModel() {
|
||||
private val _screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||
val screenState = _screenState.asStateFlow()
|
||||
|
||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||
override val loginDialog = MutableStateFlow<LoginDialog?>(null)
|
||||
private val _loginDialog = MutableStateFlow<LoginDialog?>(null)
|
||||
val loginDialog = _loginDialog.asStateFlow()
|
||||
|
||||
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||
private val _validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||
val validationArguments = _validationArguments.asStateFlow()
|
||||
|
||||
override val isNeedToClearCaptchaCode = MutableStateFlow(false)
|
||||
override val isNeedToClearValidationCode = MutableStateFlow(false)
|
||||
private val _captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||
val captchaArguments = _captchaArguments.asStateFlow()
|
||||
|
||||
private val _userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||
val userBannedArguments = _userBannedArguments.asStateFlow()
|
||||
|
||||
private val _isNeedToOpenMain = MutableStateFlow(false)
|
||||
val isNeedToOpenMain = _isNeedToOpenMain.asStateFlow()
|
||||
|
||||
private val _isNeedToClearCaptchaCode = MutableStateFlow(false)
|
||||
val isNeedToClearCaptchaCode = _isNeedToClearCaptchaCode.asStateFlow()
|
||||
|
||||
private val _isNeedToClearValidationCode = MutableStateFlow(false)
|
||||
val isNeedToClearValidationCode = _isNeedToClearValidationCode.asStateFlow()
|
||||
|
||||
private val validationState: StateFlow<List<LoginValidationResult>> =
|
||||
screenState.map(loginValidator::validate)
|
||||
@@ -115,7 +96,7 @@ class LoginViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) {
|
||||
fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) {
|
||||
onDialogDismissed(dialog)
|
||||
|
||||
when (dialog) {
|
||||
@@ -123,20 +104,24 @@ class LoginViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogDismissed(dialog: LoginDialog) {
|
||||
loginDialog.setValue { null }
|
||||
fun onDialogDismissed(dialog: LoginDialog) {
|
||||
when (dialog) {
|
||||
is LoginDialog.Error -> Unit
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
screenState.setValue { old -> old.copy(showLogo = true) }
|
||||
_loginDialog.setValue { null }
|
||||
}
|
||||
|
||||
override fun onPasswordVisibilityButtonClicked() {
|
||||
screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
|
||||
fun onBackPressed() {
|
||||
_screenState.setValue { old -> old.copy(showLogo = true) }
|
||||
}
|
||||
|
||||
override fun onLoginInputChanged(newLogin: String) {
|
||||
screenState.setValue { old ->
|
||||
fun onPasswordVisibilityButtonClicked() {
|
||||
_screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
|
||||
}
|
||||
|
||||
fun onLoginInputChanged(newLogin: String) {
|
||||
_screenState.setValue { old ->
|
||||
old.copy(
|
||||
login = newLogin.trim(),
|
||||
loginError = false
|
||||
@@ -144,8 +129,8 @@ class LoginViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPasswordInputChanged(newPassword: String) {
|
||||
screenState.setValue { old ->
|
||||
fun onPasswordInputChanged(newPassword: String) {
|
||||
_screenState.setValue { old ->
|
||||
old.copy(
|
||||
password = newPassword.trim(),
|
||||
passwordError = false
|
||||
@@ -153,47 +138,55 @@ class LoginViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSignInButtonClicked() {
|
||||
fun onSignInButtonClicked() {
|
||||
if (screenState.value.isLoading) return
|
||||
|
||||
if (screenState.value.showLogo) {
|
||||
screenState.setValue { old -> old.copy(showLogo = false) }
|
||||
_screenState.setValue { old -> old.copy(showLogo = false) }
|
||||
return
|
||||
}
|
||||
|
||||
login()
|
||||
}
|
||||
|
||||
override fun onNavigatedToMain() {
|
||||
isNeedToOpenMain.update { false }
|
||||
fun onLogoClicked() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
userSettings.onEnableDynamicColorsChanged(
|
||||
!userSettings.enableDynamicColors.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigatedToUserBanned() {
|
||||
userBannedArguments.update { null }
|
||||
fun onNavigatedToMain() {
|
||||
_isNeedToOpenMain.update { false }
|
||||
}
|
||||
|
||||
override fun onNavigatedToCaptcha() {
|
||||
captchaArguments.update { null }
|
||||
fun onNavigatedToUserBanned() {
|
||||
_userBannedArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onNavigatedToValidation() {
|
||||
validationArguments.update { null }
|
||||
fun onNavigatedToCaptcha() {
|
||||
_captchaArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onValidationCodeReceived(code: String?) {
|
||||
fun onNavigatedToValidation() {
|
||||
_validationArguments.update { null }
|
||||
}
|
||||
|
||||
fun onValidationCodeReceived(code: String?) {
|
||||
validationCode.update { code }
|
||||
}
|
||||
|
||||
override fun onValidationCodeCleared() {
|
||||
isNeedToClearValidationCode.update { false }
|
||||
fun onValidationCodeCleared() {
|
||||
_isNeedToClearValidationCode.update { false }
|
||||
}
|
||||
|
||||
override fun onCaptchaCodeReceived(code: String?) {
|
||||
fun onCaptchaCodeReceived(code: String?) {
|
||||
captchaCode.update { code }
|
||||
}
|
||||
|
||||
override fun onCaptchaCodeCleared() {
|
||||
isNeedToClearCaptchaCode.update { false }
|
||||
fun onCaptchaCodeCleared() {
|
||||
_isNeedToClearCaptchaCode.update { false }
|
||||
}
|
||||
|
||||
private fun login(forceSms: Boolean = false) {
|
||||
@@ -210,7 +203,7 @@ class LoginViewModelImpl(
|
||||
processValidation()
|
||||
if (!validationState.value.contains(LoginValidationResult.Valid)) return
|
||||
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
_screenState.updateValue { copy(isLoading = true) }
|
||||
|
||||
val currentValidationSid = validationSid.value
|
||||
val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null }
|
||||
@@ -229,7 +222,7 @@ class LoginViewModelImpl(
|
||||
error = { error ->
|
||||
Log.d("LoginViewModelImpl", "login: error: $error")
|
||||
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
_screenState.updateValue { copy(isLoading = false) }
|
||||
captchaSid.setValue { null }
|
||||
|
||||
parseError(error)
|
||||
@@ -237,8 +230,8 @@ class LoginViewModelImpl(
|
||||
success = { response ->
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, _ ->
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
_screenState.updateValue { copy(isLoading = false) }
|
||||
_loginDialog.setValue { LoginDialog.Error() }
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
|
||||
@@ -264,8 +257,8 @@ class LoginViewModelImpl(
|
||||
}
|
||||
|
||||
if (exchangeToken == null) {
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
_screenState.updateValue { copy(isLoading = false) }
|
||||
_loginDialog.setValue { LoginDialog.Error() }
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -303,15 +296,15 @@ class LoginViewModelImpl(
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
any = {
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
_screenState.updateValue { copy(isLoading = false) }
|
||||
},
|
||||
error = ::parseError,
|
||||
success = { user ->
|
||||
if (user == null) {
|
||||
loginDialog.update { LoginDialog.Error() }
|
||||
_loginDialog.update { LoginDialog.Error() }
|
||||
} else {
|
||||
screenState.updateValue { copy(login = "", password = "") }
|
||||
isNeedToOpenMain.update { true }
|
||||
_screenState.updateValue { copy(login = "", password = "") }
|
||||
_isNeedToOpenMain.update { true }
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -334,7 +327,7 @@ class LoginViewModelImpl(
|
||||
validationType = error.validationType.value,
|
||||
canResendSms = error.validationResend == "sms"
|
||||
)
|
||||
validationArguments.update { arguments }
|
||||
_validationArguments.update { arguments }
|
||||
validationSid.update { error.validationSid }
|
||||
}
|
||||
|
||||
@@ -343,12 +336,12 @@ class LoginViewModelImpl(
|
||||
captchaSid = error.captchaSid,
|
||||
captchaImageUrl = error.captchaImageUrl
|
||||
)
|
||||
captchaArguments.update { arguments }
|
||||
_captchaArguments.update { arguments }
|
||||
captchaSid.update { error.captchaSid }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.InvalidCredentialsError -> {
|
||||
loginDialog.setValue {
|
||||
_loginDialog.setValue {
|
||||
LoginDialog.Error(errorText = "Wrong login or password.")
|
||||
}
|
||||
}
|
||||
@@ -360,33 +353,33 @@ class LoginViewModelImpl(
|
||||
restoreUrl = error.restoreUrl,
|
||||
accessToken = error.accessToken
|
||||
)
|
||||
userBannedArguments.update { arguments }
|
||||
_userBannedArguments.update { arguments }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongValidationCode -> {
|
||||
isNeedToClearValidationCode.update { true }
|
||||
_isNeedToClearValidationCode.update { true }
|
||||
validationCode.update { null }
|
||||
loginDialog.setValue {
|
||||
_loginDialog.setValue {
|
||||
LoginDialog.Error(errorText = "Wrong validation code.")
|
||||
}
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
||||
isNeedToClearValidationCode.update { true }
|
||||
_isNeedToClearValidationCode.update { true }
|
||||
validationCode.update { null }
|
||||
loginDialog.setValue {
|
||||
_loginDialog.setValue {
|
||||
LoginDialog.Error(errorText = "Wrong validation code format.")
|
||||
}
|
||||
}
|
||||
|
||||
OAuthErrorDomain.TooManyTriesError -> {
|
||||
loginDialog.setValue {
|
||||
_loginDialog.setValue {
|
||||
LoginDialog.Error(errorText = "Too many tries. Try in another hour or later.")
|
||||
}
|
||||
}
|
||||
|
||||
OAuthErrorDomain.UnknownError -> {
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
_loginDialog.setValue { LoginDialog.Error() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,11 +392,11 @@ class LoginViewModelImpl(
|
||||
validationState.value.forEach { result ->
|
||||
when (result) {
|
||||
LoginValidationResult.LoginEmpty -> {
|
||||
screenState.setValue { old -> old.copy(loginError = true) }
|
||||
_screenState.setValue { old -> old.copy(loginError = true) }
|
||||
}
|
||||
|
||||
LoginValidationResult.PasswordEmpty -> {
|
||||
screenState.setValue { old -> old.copy(passwordError = true) }
|
||||
_screenState.setValue { old -> old.copy(passwordError = true) }
|
||||
}
|
||||
|
||||
LoginValidationResult.Empty -> Unit
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package dev.meloda.fast.auth.login.di
|
||||
|
||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import dev.meloda.fast.auth.login.LoginViewModel
|
||||
import dev.meloda.fast.auth.login.validation.LoginValidator
|
||||
import dev.meloda.fast.domain.OAuthUseCase
|
||||
import dev.meloda.fast.domain.OAuthUseCaseImpl
|
||||
import dev.meloda.fast.auth.login.validation.LoginValidator
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
@@ -11,6 +11,6 @@ import org.koin.dsl.module
|
||||
|
||||
val loginModule = module {
|
||||
singleOf(::LoginValidator)
|
||||
viewModelOf(::LoginViewModelImpl) bind dev.meloda.fast.auth.login.LoginViewModel::class
|
||||
viewModelOf(::LoginViewModel)
|
||||
singleOf(::OAuthUseCaseImpl) bind OAuthUseCase::class
|
||||
}
|
||||
|
||||
+4
-2
@@ -3,12 +3,12 @@ package dev.meloda.fast.auth.login.navigation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import dev.meloda.fast.auth.login.LoginViewModel
|
||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||
@@ -24,11 +24,12 @@ fun NavGraphBuilder.loginScreen(
|
||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
composable<Login> { backStackEntry ->
|
||||
val viewModel: LoginViewModel =
|
||||
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||
backStackEntry.sharedViewModel<LoginViewModel>(navController = navController)
|
||||
|
||||
val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle()
|
||||
val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle()
|
||||
@@ -55,6 +56,7 @@ fun NavGraphBuilder.loginScreen(
|
||||
onNavigateToMain = onNavigateToMain,
|
||||
onNavigateToCaptcha = onNavigateToCaptcha,
|
||||
onNavigateToValidation = onNavigateToValidation,
|
||||
onNavigateToSettings = onNavigateToSettings,
|
||||
validationCode = validationCode,
|
||||
captchaCode = captchaCode,
|
||||
viewModel = viewModel
|
||||
|
||||
+120
-36
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.auth.login.presentation
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -8,6 +9,7 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -27,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -36,34 +39,37 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentType
|
||||
import androidx.compose.ui.semantics.password
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.meloda.fast.auth.login.LoginViewModel
|
||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import dev.meloda.fast.auth.login.model.LoginDialog
|
||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.common.LocalSizeConfig
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||
import dev.meloda.fast.ui.util.handleEnterKey
|
||||
import dev.meloda.fast.ui.util.handleTabKey
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun LoginRoute(
|
||||
@@ -71,9 +77,10 @@ fun LoginRoute(
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
validationCode: String?,
|
||||
captchaCode: String?,
|
||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||
viewModel: LoginViewModel = koinViewModel()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||
@@ -126,7 +133,9 @@ fun LoginRoute(
|
||||
onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked,
|
||||
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
||||
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
||||
onSignInButtonClicked = viewModel::onSignInButtonClicked
|
||||
onSignInButtonClicked = viewModel::onSignInButtonClicked,
|
||||
onLogoClicked = viewModel::onLogoClicked,
|
||||
onLogoLongClicked = onNavigateToSettings
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
@@ -144,13 +153,22 @@ fun LoginScreen(
|
||||
onPasswordFieldEnterKeyClicked: () -> Unit = {},
|
||||
onPasswordVisibilityButtonClicked: () -> Unit = {},
|
||||
onPasswordFieldGoAction: () -> Unit = {},
|
||||
onSignInButtonClicked: () -> Unit = {}
|
||||
onSignInButtonClicked: () -> Unit = {},
|
||||
onLogoClicked: () -> Unit = {},
|
||||
onLogoLongClicked: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val size = LocalSizeConfig.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val titleSpacerSize by animateDpAsState(if (size.isHeightSmall) 24.dp else 58.dp)
|
||||
val bottomPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
|
||||
val titleSpacerSize by animateDpAsState(
|
||||
targetValue = if (size.isHeightSmall) 24.dp else 58.dp,
|
||||
label = "title spacer size"
|
||||
)
|
||||
val bottomPadding by animateDpAsState(
|
||||
targetValue = if (size.isHeightSmall) 10.dp else 30.dp,
|
||||
label = "bottom padding"
|
||||
)
|
||||
|
||||
val (loginFocusable, passwordFocusable) =
|
||||
FocusRequester.createRefs()
|
||||
@@ -163,15 +181,18 @@ fun LoginScreen(
|
||||
.padding(padding)
|
||||
.padding(top = 30.dp)
|
||||
.padding(horizontal = 30.dp)
|
||||
.padding(bottom = bottomPadding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = screenState.showLogo,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
exit = fadeOut(),
|
||||
label = "Logo visibility"
|
||||
) {
|
||||
Logo()
|
||||
Logo(
|
||||
onLogoClicked = onLogoClicked,
|
||||
onLogoLongClicked = onLogoLongClicked
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
@@ -180,7 +201,8 @@ fun LoginScreen(
|
||||
.align(Alignment.Center),
|
||||
visible = !screenState.showLogo,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
exit = fadeOut(),
|
||||
label = "Login visibility"
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -188,7 +210,7 @@ fun LoginScreen(
|
||||
.align(Alignment.Center)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = UiR.string.sign_in_to_vk),
|
||||
text = stringResource(id = R.string.sign_in_to_vk),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.displayMedium
|
||||
)
|
||||
@@ -214,11 +236,11 @@ fun LoginScreen(
|
||||
},
|
||||
value = screenState.login,
|
||||
onValueChange = onLoginInputChanged,
|
||||
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
||||
placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
||||
label = { Text(text = stringResource(id = R.string.login_hint)) },
|
||||
placeholder = { Text(text = stringResource(id = R.string.login_hint)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_round_person_24),
|
||||
painter = painterResource(id = R.drawable.ic_round_person_24),
|
||||
contentDescription = "Login icon",
|
||||
tint = if (screenState.loginError) {
|
||||
MaterialTheme.colorScheme.error
|
||||
@@ -235,8 +257,11 @@ fun LoginScreen(
|
||||
isError = screenState.loginError,
|
||||
singleLine = true
|
||||
)
|
||||
AnimatedVisibility(visible = screenState.loginError) {
|
||||
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
|
||||
AnimatedVisibility(
|
||||
visible = screenState.loginError,
|
||||
label = "Login error visibility"
|
||||
) {
|
||||
TextFieldErrorText(text = stringResource(id = R.string.error_empty_field))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -255,11 +280,11 @@ fun LoginScreen(
|
||||
.semantics { contentType = ContentType.Password },
|
||||
value = screenState.password,
|
||||
onValueChange = onPasswordInputChanged,
|
||||
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
||||
placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
||||
label = { Text(text = stringResource(id = R.string.password_login_hint)) },
|
||||
placeholder = { Text(text = stringResource(id = R.string.password_login_hint)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.round_vpn_key_24),
|
||||
painter = painterResource(id = R.drawable.round_vpn_key_24),
|
||||
contentDescription = "Password icon",
|
||||
tint = if (screenState.passwordError) {
|
||||
MaterialTheme.colorScheme.error
|
||||
@@ -270,8 +295,8 @@ fun LoginScreen(
|
||||
},
|
||||
trailingIcon = {
|
||||
val imagePainter = painterResource(
|
||||
id = if (screenState.passwordVisible) UiR.drawable.round_visibility_off_24
|
||||
else UiR.drawable.round_visibility_24
|
||||
id = if (screenState.passwordVisible) R.drawable.round_visibility_off_24
|
||||
else R.drawable.round_visibility_24px
|
||||
)
|
||||
|
||||
IconButton(onClick = onPasswordVisibilityButtonClicked) {
|
||||
@@ -300,16 +325,18 @@ fun LoginScreen(
|
||||
},
|
||||
singleLine = true
|
||||
)
|
||||
AnimatedVisibility(visible = screenState.passwordError) {
|
||||
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
|
||||
AnimatedVisibility(
|
||||
visible = screenState.passwordError,
|
||||
label = "Password error visibility"
|
||||
) {
|
||||
TextFieldErrorText(text = stringResource(id = R.string.error_empty_field))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Box(
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
contentAlignment = Alignment.Center
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
@@ -324,7 +351,8 @@ fun LoginScreen(
|
||||
AnimatedVisibility(
|
||||
visible = screenState.isLoading,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
exit = fadeOut(),
|
||||
label = "Progress indicator visibility"
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
@@ -332,21 +360,67 @@ fun LoginScreen(
|
||||
AnimatedVisibility(
|
||||
visible = !screenState.isLoading,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
exit = fadeOut(),
|
||||
label = "Sign in icon visibility"
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_arrow_end),
|
||||
painter = painterResource(id = R.drawable.ic_arrow_end),
|
||||
contentDescription = "Sign in icon",
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = screenState.showLogo,
|
||||
label = "Bottom padding visibility"
|
||||
) {
|
||||
Spacer(Modifier.height(bottomPadding))
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !screenState.showLogo,
|
||||
label = "Spacer between fab and bottom text buttons visibility"
|
||||
) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !screenState.showLogo,
|
||||
label = "Text button row visibility"
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, "https://vk.ru/join".toUri())
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.login_sign_up))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "•",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, "https://vk.ru/restore".toUri())
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.login_forgot_password))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun HandleDialogs(
|
||||
loginDialog: LoginDialog?,
|
||||
@@ -359,12 +433,22 @@ fun HandleDialogs(
|
||||
is LoginDialog.Error -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(loginDialog) },
|
||||
title = stringResource(UiR.string.title_error),
|
||||
title = stringResource(R.string.title_error),
|
||||
text = loginDialog.errorTextResId?.let { stringResource(it) }
|
||||
?: loginDialog.errorText
|
||||
?: stringResource(UiR.string.unknown_error_occurred),
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
?: stringResource(R.string.unknown_error_occurred),
|
||||
confirmText = stringResource(id = R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LoginScreenPreview() {
|
||||
LoginScreen(
|
||||
screenState = LoginScreenState.EMPTY.copy(
|
||||
showLogo = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package dev.meloda.fast.auth.login.presentation
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -26,22 +24,21 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||
import org.koin.compose.koinInject
|
||||
import dev.meloda.fast.ui.common.LocalSizeConfig
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Logo(modifier: Modifier = Modifier) {
|
||||
fun Logo(
|
||||
modifier: Modifier = Modifier,
|
||||
onLogoClicked: () -> Unit = {},
|
||||
onLogoLongClicked: () -> Unit = {}
|
||||
) {
|
||||
val size = LocalSizeConfig.current
|
||||
|
||||
val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp)
|
||||
val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 38)
|
||||
val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@@ -64,14 +61,8 @@ fun Logo(modifier: Modifier = Modifier) {
|
||||
.combinedClickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
onLongClick = null,
|
||||
onClick = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
userSettings.onEnableDynamicColorsChanged(
|
||||
!userSettings.enableDynamicColors.value
|
||||
)
|
||||
}
|
||||
}
|
||||
onLongClick = onLogoLongClicked,
|
||||
onClick = onLogoClicked
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -23,5 +23,4 @@ class LoginValidator {
|
||||
|
||||
return resultList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-8
@@ -4,8 +4,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -16,6 +14,7 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -24,7 +23,7 @@ import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.auth.userbanned.model.UserBannedScreenState
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -68,13 +67,13 @@ fun UserBannedScreen(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
painter = painterResource(R.drawable.round_arrow_back_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(id = UiR.string.warning))
|
||||
Text(text = stringResource(id = R.string.warning))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -86,7 +85,7 @@ fun UserBannedScreen(
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = UiR.string.account_temporarily_blocked),
|
||||
text = stringResource(id = R.string.account_temporarily_blocked),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
|
||||
)
|
||||
@@ -94,7 +93,7 @@ fun UserBannedScreen(
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
|
||||
append(stringResource(id = UiR.string.user_name))
|
||||
append(stringResource(id = R.string.user_name))
|
||||
append(": ")
|
||||
}
|
||||
|
||||
@@ -104,7 +103,7 @@ fun UserBannedScreen(
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
|
||||
append(stringResource(id = UiR.string.blocking_reason_title))
|
||||
append(stringResource(id = R.string.blocking_reason_title))
|
||||
append(": ")
|
||||
}
|
||||
append(screenState.message)
|
||||
|
||||
+9
-12
@@ -18,9 +18,6 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -56,11 +53,11 @@ import dev.meloda.fast.auth.validation.ValidationViewModel
|
||||
import dev.meloda.fast.auth.validation.ValidationViewModelImpl
|
||||
import dev.meloda.fast.auth.validation.model.ValidationScreenState
|
||||
import dev.meloda.fast.auth.validation.model.ValidationType
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun ValidationRoute(
|
||||
@@ -142,11 +139,11 @@ fun ValidationScreen(
|
||||
if (showExitAlert) {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { showExitAlert = false },
|
||||
title = stringResource(id = UiR.string.warning_confirmation),
|
||||
text = stringResource(id = UiR.string.validation_exit_warning),
|
||||
title = stringResource(id = R.string.warning_confirmation),
|
||||
text = stringResource(id = R.string.validation_exit_warning),
|
||||
confirmAction = { confirmedExit = true },
|
||||
confirmText = stringResource(id = UiR.string.yes),
|
||||
cancelText = stringResource(id = UiR.string.no),
|
||||
confirmText = stringResource(id = R.string.yes),
|
||||
cancelText = stringResource(id = R.string.no),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
)
|
||||
}
|
||||
@@ -173,7 +170,7 @@ fun ValidationScreen(
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
painter = painterResource(R.drawable.round_close_24px),
|
||||
contentDescription = "Close icon",
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
@@ -223,7 +220,7 @@ fun ValidationScreen(
|
||||
.semantics { contentType = ContentType.SmsOtpCode },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.round_qr_code_24),
|
||||
painter = painterResource(id = R.drawable.round_qr_code_24),
|
||||
contentDescription = "QR Code icon",
|
||||
tint = if (screenState.codeError) {
|
||||
MaterialTheme.colorScheme.error
|
||||
@@ -271,7 +268,7 @@ fun ValidationScreen(
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.round_sms_24),
|
||||
painter = painterResource(id = R.drawable.round_sms_24),
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
contentDescription = "SMS icon"
|
||||
)
|
||||
@@ -287,7 +284,7 @@ fun ValidationScreen(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Done,
|
||||
painter = painterResource(R.drawable.round_check_24px),
|
||||
contentDescription = "Done icon",
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
|
||||
+13
-22
@@ -11,17 +11,13 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRowDefaults
|
||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -36,6 +32,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -52,13 +49,13 @@ import dev.meloda.fast.chatmaterials.presentation.materials.FileMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.LinkMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.PhotoMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.VideoMaterialsScreen
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.model.TabItem
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun ChatMaterialsRoute(
|
||||
@@ -84,15 +81,15 @@ fun ChatMaterialsScreen(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val hazeState = remember { HazeState() }
|
||||
val hazeState = remember { HazeState(true) }
|
||||
|
||||
val titles = remember {
|
||||
listOf(
|
||||
UiR.string.chat_attachment_photos,
|
||||
UiR.string.chat_attachment_videos,
|
||||
UiR.string.chat_attachment_music,
|
||||
UiR.string.chat_attachment_files,
|
||||
UiR.string.chat_attachment_links,
|
||||
R.string.chat_attachment_photos,
|
||||
R.string.chat_attachment_videos,
|
||||
R.string.chat_attachment_music,
|
||||
R.string.chat_attachment_files,
|
||||
R.string.chat_attachment_links,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,7 +154,7 @@ fun ChatMaterialsScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(UiR.string.chat_materials_title),
|
||||
text = stringResource(R.string.chat_materials_title),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
@@ -170,23 +167,17 @@ fun ChatMaterialsScreen(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
painter = painterResource(R.drawable.round_arrow_back_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ScrollableTabRow(
|
||||
PrimaryScrollableTabRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
containerColor = Color.Transparent,
|
||||
edgePadding = 0.dp,
|
||||
indicator = { tabPositions ->
|
||||
TabRowDefaults.PrimaryIndicator(
|
||||
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
edgePadding = 0.dp
|
||||
) {
|
||||
tabItems.forEachIndexed { index, item ->
|
||||
Tab(
|
||||
|
||||
+4
-7
@@ -18,8 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -53,14 +51,13 @@ import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.components.VkErrorView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -105,7 +102,7 @@ fun AudioMaterialsScreen(
|
||||
VkErrorView(baseError = baseError)
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
@@ -158,7 +155,7 @@ fun AudioMaterialsScreen(
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.size(42.dp)
|
||||
.padding(4.dp),
|
||||
painter = painterResource(UiR.drawable.round_play_arrow_24),
|
||||
painter = painterResource(R.drawable.round_fill_play_arrow_24px),
|
||||
contentDescription = null,
|
||||
tint = contentColorFor(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
@@ -206,7 +203,7 @@ fun AudioMaterialsScreen(
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
painter = painterResource(R.drawable.round_keyboard_arrow_up_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
+4
-5
@@ -19,8 +19,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -46,6 +44,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -63,7 +62,7 @@ import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.components.VkErrorView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
@@ -114,7 +113,7 @@ fun FileMaterialsScreen(
|
||||
VkErrorView(baseError = baseError)
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
@@ -238,7 +237,7 @@ fun FileMaterialsScreen(
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
painter = painterResource(R.drawable.round_keyboard_arrow_up_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
+4
-5
@@ -19,8 +19,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -46,6 +44,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -63,7 +62,7 @@ import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.components.VkErrorView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
@@ -114,7 +113,7 @@ fun LinkMaterialsScreen(
|
||||
VkErrorView(baseError = baseError)
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
@@ -257,7 +256,7 @@ fun LinkMaterialsScreen(
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
painter = painterResource(R.drawable.round_keyboard_arrow_up_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
+4
-5
@@ -17,8 +17,6 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -37,6 +35,7 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -46,7 +45,7 @@ import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.components.VkErrorView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
@@ -98,7 +97,7 @@ fun PhotoMaterialsScreen(
|
||||
VkErrorView(baseError = baseError)
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
@@ -178,7 +177,7 @@ fun PhotoMaterialsScreen(
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
painter = painterResource(R.drawable.round_keyboard_arrow_up_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
+4
-5
@@ -19,8 +19,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -43,6 +41,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
@@ -56,7 +55,7 @@ import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.FullScreenContainedLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.components.VkErrorView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
@@ -107,7 +106,7 @@ fun VideoMaterialsScreen(
|
||||
VkErrorView(baseError = baseError)
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
@@ -230,7 +229,7 @@ fun VideoMaterialsScreen(
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
painter = painterResource(R.drawable.round_keyboard_arrow_up_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
+73
-101
@@ -38,54 +38,15 @@ import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
interface ConversationsViewModel {
|
||||
|
||||
val screenState: StateFlow<ConversationsScreenState>
|
||||
val navigation: StateFlow<ConversationNavigation?>
|
||||
val dialog: StateFlow<ConversationDialog?>
|
||||
|
||||
val conversations: StateFlow<List<VkConversation>>
|
||||
val uiConversations: StateFlow<List<UiConversation>>
|
||||
|
||||
val baseError: StateFlow<BaseError?>
|
||||
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
fun onNavigationConsumed()
|
||||
|
||||
fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle)
|
||||
fun onDialogDismissed(dialog: ConversationDialog)
|
||||
fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle)
|
||||
|
||||
fun onErrorButtonClicked()
|
||||
|
||||
fun onPaginationConditionsMet()
|
||||
|
||||
fun onOptionClicked(conversation: UiConversation, option: ConversationOption)
|
||||
|
||||
fun onRefresh()
|
||||
|
||||
fun onConversationItemClick(conversation: UiConversation)
|
||||
fun onConversationItemLongClick(conversation: UiConversation)
|
||||
|
||||
fun onErrorConsumed()
|
||||
|
||||
fun setScrollIndex(index: Int)
|
||||
fun setScrollOffset(offset: Int)
|
||||
|
||||
fun onCreateChatButtonClicked()
|
||||
}
|
||||
|
||||
class ConversationsViewModelImpl(
|
||||
class ConversationsViewModel(
|
||||
updatesParser: LongPollUpdatesParser,
|
||||
private val filter: ConversationsFilter,
|
||||
private val updatesParser: LongPollUpdatesParser,
|
||||
private val conversationsUseCase: ConversationsUseCase,
|
||||
private val messagesUseCase: MessagesUseCase,
|
||||
private val resources: Resources,
|
||||
@@ -93,23 +54,34 @@ class ConversationsViewModelImpl(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val applicationContext: Context,
|
||||
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
|
||||
) : ConversationsViewModel, ViewModel() {
|
||||
) : ViewModel() {
|
||||
private val _screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
|
||||
val screenState = _screenState.asStateFlow()
|
||||
|
||||
override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
|
||||
override val navigation = MutableStateFlow<ConversationNavigation?>(null)
|
||||
override val dialog = MutableStateFlow<ConversationDialog?>(null)
|
||||
private val _navigation = MutableStateFlow<ConversationNavigation?>(null)
|
||||
val navigation = _navigation.asStateFlow()
|
||||
|
||||
override val conversations = MutableStateFlow<List<VkConversation>>(emptyList())
|
||||
override val uiConversations = MutableStateFlow<List<UiConversation>>(emptyList())
|
||||
private val _dialog = MutableStateFlow<ConversationDialog?>(null)
|
||||
val dialog = _dialog.asStateFlow()
|
||||
|
||||
private val _conversations = MutableStateFlow<List<VkConversation>>(emptyList())
|
||||
val conversations = _conversations.asStateFlow()
|
||||
|
||||
private val _uiConversations = MutableStateFlow<List<UiConversation>>(emptyList())
|
||||
val uiConversations = _uiConversations.asStateFlow()
|
||||
|
||||
private val pinnedConversationsCount = conversations.map { conversations ->
|
||||
conversations.count(VkConversation::isPinned)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
|
||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||
private val _baseError = MutableStateFlow<BaseError?>(null)
|
||||
val baseError = _baseError.asStateFlow()
|
||||
|
||||
override val currentOffset = MutableStateFlow(0)
|
||||
override val canPaginate = MutableStateFlow(false)
|
||||
private val _currentOffset = MutableStateFlow(0)
|
||||
val currentOffset = _currentOffset.asStateFlow()
|
||||
|
||||
private val _canPaginate = MutableStateFlow(false)
|
||||
val canPaginate = _canPaginate.asStateFlow()
|
||||
|
||||
private val expandedConversationId = MutableStateFlow(0L)
|
||||
|
||||
@@ -118,7 +90,7 @@ class ConversationsViewModelImpl(
|
||||
private val interactionsTimers = hashMapOf<Long, InteractionJob?>()
|
||||
|
||||
init {
|
||||
screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) }
|
||||
_screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) }
|
||||
|
||||
loadConversations()
|
||||
|
||||
@@ -137,11 +109,11 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationConsumed() {
|
||||
navigation.setValue { null }
|
||||
fun onNavigationConsumed() {
|
||||
_navigation.setValue { null }
|
||||
}
|
||||
|
||||
override fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) {
|
||||
fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) {
|
||||
onDialogDismissed(dialog)
|
||||
|
||||
when (dialog) {
|
||||
@@ -170,11 +142,11 @@ class ConversationsViewModelImpl(
|
||||
syncUiConversation()
|
||||
}
|
||||
|
||||
override fun onDialogDismissed(dialog: ConversationDialog) {
|
||||
this.dialog.setValue { null }
|
||||
fun onDialogDismissed(dialog: ConversationDialog) {
|
||||
_dialog.setValue { null }
|
||||
}
|
||||
|
||||
override fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) {
|
||||
fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) {
|
||||
when (dialog) {
|
||||
is ConversationDialog.ConversationDelete -> Unit
|
||||
is ConversationDialog.ConversationPin -> Unit
|
||||
@@ -184,7 +156,7 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onErrorButtonClicked() {
|
||||
fun onErrorButtonClicked() {
|
||||
when (baseError.value) {
|
||||
null -> Unit
|
||||
|
||||
@@ -197,22 +169,22 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { conversations.value.size }
|
||||
fun onPaginationConditionsMet() {
|
||||
_currentOffset.update { conversations.value.size }
|
||||
loadConversations()
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
fun onRefresh() {
|
||||
onErrorConsumed()
|
||||
loadConversations(offset = 0)
|
||||
}
|
||||
|
||||
override fun onConversationItemClick(conversation: UiConversation) {
|
||||
fun onConversationItemClick(conversation: UiConversation) {
|
||||
collapseConversations()
|
||||
navigation.setValue { ConversationNavigation.MessagesHistory(peerId = conversation.id) }
|
||||
_navigation.setValue { ConversationNavigation.MessagesHistory(peerId = conversation.id) }
|
||||
}
|
||||
|
||||
override fun onConversationItemLongClick(conversation: UiConversation) {
|
||||
fun onConversationItemLongClick(conversation: UiConversation) {
|
||||
expandedConversationId.setValue {
|
||||
if (conversation.isExpanded) 0
|
||||
else conversation.id
|
||||
@@ -220,13 +192,13 @@ class ConversationsViewModelImpl(
|
||||
syncUiConversation()
|
||||
}
|
||||
|
||||
override fun onOptionClicked(
|
||||
fun onOptionClicked(
|
||||
conversation: UiConversation,
|
||||
option: ConversationOption
|
||||
) {
|
||||
when (option) {
|
||||
ConversationOption.Delete -> {
|
||||
dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) }
|
||||
_dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) }
|
||||
}
|
||||
|
||||
ConversationOption.MarkAsRead -> {
|
||||
@@ -240,37 +212,37 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
|
||||
ConversationOption.Pin -> {
|
||||
dialog.setValue { ConversationDialog.ConversationPin(conversation.id) }
|
||||
_dialog.setValue { ConversationDialog.ConversationPin(conversation.id) }
|
||||
}
|
||||
|
||||
ConversationOption.Unpin -> {
|
||||
dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) }
|
||||
_dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) }
|
||||
}
|
||||
|
||||
ConversationOption.Archive -> {
|
||||
dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) }
|
||||
_dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) }
|
||||
}
|
||||
|
||||
ConversationOption.Unarchive -> {
|
||||
dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) }
|
||||
_dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onErrorConsumed() {
|
||||
baseError.setValue { null }
|
||||
fun onErrorConsumed() {
|
||||
_baseError.setValue { null }
|
||||
}
|
||||
|
||||
override fun setScrollIndex(index: Int) {
|
||||
screenState.setValue { old -> old.copy(scrollIndex = index) }
|
||||
fun setScrollIndex(index: Int) {
|
||||
_screenState.setValue { old -> old.copy(scrollIndex = index) }
|
||||
}
|
||||
|
||||
override fun setScrollOffset(offset: Int) {
|
||||
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
||||
fun setScrollOffset(offset: Int) {
|
||||
_screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
||||
}
|
||||
|
||||
override fun onCreateChatButtonClicked() {
|
||||
navigation.setValue { ConversationNavigation.CreateChat }
|
||||
fun onCreateChatButtonClicked() {
|
||||
_navigation.setValue { ConversationNavigation.CreateChat }
|
||||
}
|
||||
|
||||
private fun collapseConversations() {
|
||||
@@ -289,7 +261,7 @@ class ConversationsViewModelImpl(
|
||||
state.processState(
|
||||
error = { error ->
|
||||
val newBaseError = VkUtils.parseError(error)
|
||||
baseError.update { newBaseError }
|
||||
_baseError.update { newBaseError }
|
||||
},
|
||||
success = { response ->
|
||||
val conversations = response
|
||||
@@ -304,7 +276,7 @@ class ConversationsViewModelImpl(
|
||||
val paginationExhausted = !itemsCountSufficient &&
|
||||
this.conversations.value.isNotEmpty()
|
||||
|
||||
screenState.updateValue {
|
||||
_screenState.updateValue {
|
||||
copy(isPaginationExhausted = paginationExhausted)
|
||||
}
|
||||
|
||||
@@ -321,13 +293,13 @@ class ConversationsViewModelImpl(
|
||||
|
||||
conversationsUseCase.storeConversations(response)
|
||||
|
||||
this.conversations.emit(fullConversations)
|
||||
_conversations.emit(fullConversations)
|
||||
syncUiConversation()
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
_canPaginate.setValue { itemsCountSufficient }
|
||||
}
|
||||
)
|
||||
|
||||
screenState.setValue { old ->
|
||||
_screenState.setValue { old ->
|
||||
old.copy(
|
||||
isLoading = offset == 0 && state.isLoading(),
|
||||
isPaginating = offset > 0 && state.isLoading()
|
||||
@@ -347,11 +319,11 @@ class ConversationsViewModelImpl(
|
||||
?: return@processState
|
||||
|
||||
newConversations.removeAt(conversationIndex)
|
||||
conversations.update { newConversations.sorted() }
|
||||
_conversations.update { newConversations.sorted() }
|
||||
syncUiConversation()
|
||||
}
|
||||
)
|
||||
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
||||
_screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +346,7 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
)
|
||||
|
||||
screenState.setValue { old -> old.copy(isLoading = state.isLoading()) }
|
||||
_screenState.setValue { old -> old.copy(isLoading = state.isLoading()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +392,7 @@ class ConversationsViewModelImpl(
|
||||
.copy(lastMessage = message)
|
||||
|
||||
newConversations.add(pinnedConversationsCount.value, conversation)
|
||||
conversations.update { newConversations.sorted() }
|
||||
_conversations.update { newConversations.sorted() }
|
||||
syncUiConversation()
|
||||
}
|
||||
)
|
||||
@@ -461,7 +433,7 @@ class ConversationsViewModelImpl(
|
||||
newConversations.add(toPosition, newConversation)
|
||||
}
|
||||
|
||||
conversations.update { newConversations.sorted() }
|
||||
_conversations.update { newConversations.sorted() }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -480,7 +452,7 @@ class ConversationsViewModelImpl(
|
||||
lastMessageId = message.id,
|
||||
lastCmId = message.cmId
|
||||
)
|
||||
conversations.update { newConversations }
|
||||
_conversations.update { newConversations }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -500,7 +472,7 @@ class ConversationsViewModelImpl(
|
||||
unreadCount = event.unreadCount
|
||||
)
|
||||
|
||||
conversations.update { newConversations }
|
||||
_conversations.update { newConversations }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -520,7 +492,7 @@ class ConversationsViewModelImpl(
|
||||
unreadCount = event.unreadCount
|
||||
)
|
||||
|
||||
conversations.update { newConversations }
|
||||
_conversations.update { newConversations }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -541,7 +513,7 @@ class ConversationsViewModelImpl(
|
||||
interactionIds = userIds
|
||||
)
|
||||
|
||||
conversations.update { newConversations }
|
||||
_conversations.update { newConversations }
|
||||
syncUiConversation()
|
||||
|
||||
interactionsTimers[peerId]?.let { interactionJob ->
|
||||
@@ -583,7 +555,7 @@ class ConversationsViewModelImpl(
|
||||
interactionIds = emptyList()
|
||||
)
|
||||
|
||||
conversations.update { newConversations }
|
||||
_conversations.update { newConversations }
|
||||
syncUiConversation()
|
||||
|
||||
interactionJob.timerJob.cancel()
|
||||
@@ -601,7 +573,7 @@ class ConversationsViewModelImpl(
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(majorId = event.majorId)
|
||||
|
||||
conversations.setValue { newConversations.sorted() }
|
||||
_conversations.setValue { newConversations.sorted() }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -617,7 +589,7 @@ class ConversationsViewModelImpl(
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(minorId = event.minorId)
|
||||
|
||||
conversations.setValue { newConversations.sorted() }
|
||||
_conversations.setValue { newConversations.sorted() }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -632,7 +604,7 @@ class ConversationsViewModelImpl(
|
||||
} else {
|
||||
newConversations.removeAt(conversationIndex)
|
||||
|
||||
conversations.setValue { newConversations.sorted() }
|
||||
_conversations.setValue { newConversations.sorted() }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -655,7 +627,7 @@ class ConversationsViewModelImpl(
|
||||
newConversations.removeAt(index)
|
||||
}
|
||||
|
||||
conversations.update { newConversations }
|
||||
_conversations.update { newConversations }
|
||||
syncUiConversation()
|
||||
}
|
||||
|
||||
@@ -669,7 +641,7 @@ class ConversationsViewModelImpl(
|
||||
newConversations.add(pinnedConversationsCount.value, conversation)
|
||||
}
|
||||
|
||||
conversations.update { newConversations.sorted() }
|
||||
_conversations.update { newConversations.sorted() }
|
||||
syncUiConversation()
|
||||
}
|
||||
}
|
||||
@@ -691,7 +663,7 @@ class ConversationsViewModelImpl(
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(inRead = startMessageId)
|
||||
|
||||
conversations.update { newConversations }
|
||||
_conversations.update { newConversations }
|
||||
syncUiConversation()
|
||||
}
|
||||
)
|
||||
@@ -763,7 +735,7 @@ class ConversationsViewModelImpl(
|
||||
options = options.toImmutableList()
|
||||
)
|
||||
}
|
||||
uiConversations.setValue { newUiConversations }
|
||||
_uiConversations.setValue { newUiConversations }
|
||||
|
||||
return newUiConversations
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
package dev.meloda.fast.conversations.di
|
||||
|
||||
import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||
import dev.meloda.fast.domain.ConversationsUseCase
|
||||
import dev.meloda.fast.domain.ConversationsUseCaseImpl
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
@@ -22,8 +22,8 @@ val conversationsModule = module {
|
||||
singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class
|
||||
}
|
||||
|
||||
private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModelImpl {
|
||||
return ConversationsViewModelImpl(
|
||||
private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModel {
|
||||
return ConversationsViewModel(
|
||||
filter = filter,
|
||||
updatesParser = get(),
|
||||
conversationsUseCase = get(),
|
||||
|
||||
+13
-19
@@ -1,19 +1,17 @@
|
||||
package dev.meloda.fast.conversations.navigation
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||
import dev.meloda.fast.conversations.presentation.ConversationsRoute
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import dev.meloda.fast.ui.extensions.getOrThrow
|
||||
import dev.meloda.fast.ui.theme.LocalNavController
|
||||
import dev.meloda.fast.ui.theme.getOrThrow
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
@Serializable
|
||||
@@ -26,6 +24,7 @@ object Conversations
|
||||
object Archive
|
||||
|
||||
fun NavGraphBuilder.conversationsGraph(
|
||||
activity: AppCompatActivity,
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToMessagesHistory: (id: Long) -> Unit,
|
||||
onNavigateToCreateChat: () -> Unit,
|
||||
@@ -34,17 +33,14 @@ fun NavGraphBuilder.conversationsGraph(
|
||||
navigation<ConversationsGraph>(
|
||||
startDestination = Conversations
|
||||
) {
|
||||
val conversationsViewModel: ConversationsViewModel = with(activity) {
|
||||
getViewModel(qualifier = named(ConversationsFilter.ALL))
|
||||
}
|
||||
composable<Conversations> {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.getOrThrow()
|
||||
|
||||
val viewModel: ConversationsViewModelImpl = koinViewModel(
|
||||
qualifier = named(ConversationsFilter.ALL),
|
||||
viewModelStoreOwner = context as AppCompatActivity
|
||||
)
|
||||
|
||||
ConversationsRoute(
|
||||
viewModel = viewModel,
|
||||
viewModel = conversationsViewModel,
|
||||
onError = onError,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
onNavigateToCreateChat = onNavigateToCreateChat,
|
||||
@@ -53,16 +49,14 @@ fun NavGraphBuilder.conversationsGraph(
|
||||
)
|
||||
}
|
||||
composable<Archive> {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.getOrThrow()
|
||||
|
||||
val viewModel: ConversationsViewModelImpl = koinViewModel(
|
||||
qualifier = named(ConversationsFilter.ARCHIVE),
|
||||
viewModelStoreOwner = context as AppCompatActivity
|
||||
)
|
||||
|
||||
ConversationsRoute(
|
||||
viewModel = viewModel,
|
||||
viewModel = with(activity) {
|
||||
getViewModel<ConversationsViewModel>(
|
||||
qualifier = named(ConversationsFilter.ARCHIVE)
|
||||
)
|
||||
},
|
||||
onBack = navController::navigateUp,
|
||||
onError = onError,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
|
||||
+16
-16
@@ -8,7 +8,7 @@ import dev.meloda.fast.conversations.model.ConversationDialog
|
||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
@Composable
|
||||
fun HandleDialogs(
|
||||
@@ -24,50 +24,50 @@ fun HandleDialogs(
|
||||
is ConversationDialog.ConversationArchive -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_archive_conversation),
|
||||
title = stringResource(id = R.string.confirm_archive_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_archive),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
confirmText = stringResource(id = R.string.action_archive),
|
||||
cancelText = stringResource(id = R.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationUnarchive -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_unarchive_conversation),
|
||||
title = stringResource(id = R.string.confirm_unarchive_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_unarchive),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
confirmText = stringResource(id = R.string.action_unarchive),
|
||||
cancelText = stringResource(id = R.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationDelete -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_delete_conversation),
|
||||
title = stringResource(id = R.string.confirm_delete_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_delete),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
confirmText = stringResource(id = R.string.action_delete),
|
||||
cancelText = stringResource(id = R.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationPin -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_pin_conversation),
|
||||
title = stringResource(id = R.string.confirm_pin_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_pin),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
confirmText = stringResource(id = R.string.action_pin),
|
||||
cancelText = stringResource(id = R.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationUnpin -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_unpin_conversation),
|
||||
title = stringResource(id = R.string.confirm_unpin_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_unpin),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
confirmText = stringResource(id = R.string.action_unpin),
|
||||
cancelText = stringResource(id = R.string.cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -53,7 +53,7 @@ import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.util.getImage
|
||||
import dev.meloda.fast.ui.util.getResourcePainter
|
||||
import dev.meloda.fast.ui.util.getString
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
val BirthdayColor = Color(0xffb00b69)
|
||||
|
||||
@@ -127,7 +127,7 @@ fun ConversationItem(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(32.dp),
|
||||
painter = painterResource(id = UiR.drawable.ic_round_bookmark_border_24),
|
||||
painter = painterResource(id = R.drawable.ic_round_bookmark_border_24),
|
||||
contentDescription = "Favorites icon",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
@@ -150,7 +150,7 @@ fun ConversationItem(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape),
|
||||
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut)
|
||||
placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ fun ConversationItem(
|
||||
modifier = Modifier
|
||||
.height(14.dp)
|
||||
.align(Alignment.Center),
|
||||
painter = painterResource(id = UiR.drawable.ic_round_push_pin_24),
|
||||
painter = painterResource(id = R.drawable.ic_round_push_pin_24),
|
||||
contentDescription = "Pin icon",
|
||||
tint = Color.White
|
||||
)
|
||||
@@ -222,7 +222,7 @@ fun ConversationItem(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(10.dp),
|
||||
painter = painterResource(id = UiR.drawable.round_cake_24),
|
||||
painter = painterResource(id = R.drawable.round_cake_24),
|
||||
contentDescription = "Birthday icon",
|
||||
tint = Color.White
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user