mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Compare commits
4 Commits
@@ -17,21 +17,22 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Описываем задачу копирования с созданием правильной структуры
|
||||
val fixComposeResourcesStructure = tasks.register<Copy>("fixComposeResourcesStructure") {
|
||||
val sharedProject = rootProject.project(":app:shared")
|
||||
|
||||
// Откуда берем (исходные сгенерированные файлы)
|
||||
val sourceDir = sharedProject.layout.buildDirectory
|
||||
.dir("generated/compose/resourceGenerator/preparedResources/commonMain/composeResources")
|
||||
from(
|
||||
sharedProject
|
||||
.layout
|
||||
.buildDirectory
|
||||
.dir("generated/compose/resourceGenerator/preparedResources/commonMain/composeResources")
|
||||
)
|
||||
|
||||
// Куда кладем (создаем промежуточную папку с ПРАВИЛЬНЫМ путем)
|
||||
val outputDir = layout.buildDirectory.dir("intermediates/fixed_compose_res/composeResources/ru.fromchat")
|
||||
into(
|
||||
layout
|
||||
.buildDirectory
|
||||
.dir("intermediates/fixed_compose_res/composeResources/ru.fromchat")
|
||||
)
|
||||
|
||||
from(sourceDir)
|
||||
into(outputDir)
|
||||
|
||||
// Убеждаемся, что генерация в shared уже прошла
|
||||
dependsOn(sharedProject.tasks.matching { it.name.contains("prepareComposeResources", ignoreCase = true) })
|
||||
dependsOn(sharedProject.tasks.matching { it.name.contains("copyNonXmlValueResources", ignoreCase = true) })
|
||||
}
|
||||
@@ -55,7 +56,7 @@ android {
|
||||
}
|
||||
|
||||
storeFile = file("keys/release.jks")
|
||||
keyAlias = "release"
|
||||
keyAlias = "key0"
|
||||
storePassword = keystoreProperties["storePassword"].toString()
|
||||
keyPassword = keystoreProperties["keyPassword"].toString()
|
||||
enableV3Signing = true
|
||||
@@ -97,6 +98,10 @@ tasks.withType<MergeSourceSetFolders>().configureEach {
|
||||
dependsOn(fixComposeResourcesStructure)
|
||||
}
|
||||
|
||||
tasks.matching { it.name.contains("lintVital", ignoreCase = true) }.configureEach {
|
||||
dependsOn(fixComposeResourcesStructure)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
BIN
Binary file not shown.
-5
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict />
|
||||
</plist>
|
||||
@@ -17,11 +17,16 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>0.2.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>021</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
@@ -46,10 +51,5 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -67,4 +67,6 @@
|
||||
<string name="typing_single">%1$s is typing…</string>
|
||||
<string name="typing_two">%1$s and %2$s are typing…</string>
|
||||
<string name="typing_many">%1$s, %2$s and %3$d more are typing…</string>
|
||||
|
||||
<string name="more">More</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,6 +21,9 @@ object Config {
|
||||
*/
|
||||
suspend fun initialize() {
|
||||
_serverConfig.value = ServerConfigStorage.getConfig()
|
||||
|
||||
// Set default values and prevent corruption
|
||||
ServerConfigStorage.getConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,19 +3,11 @@ package ru.fromchat.ui
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.End
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Start
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -25,8 +17,6 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.fromchat.api.WebSocketManager
|
||||
import ru.fromchat.core.config.Config
|
||||
import ru.fromchat.ui.auth.LoginScreen
|
||||
@@ -39,30 +29,9 @@ val LocalNavController = compositionLocalOf<NavController> { error("") }
|
||||
|
||||
@Composable
|
||||
fun App() {
|
||||
var startDestination by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// Initialize config and check server configuration on startup
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope {
|
||||
launch {
|
||||
try {
|
||||
// Initialize config
|
||||
Config.initialize()
|
||||
|
||||
// Check if server is configured
|
||||
val serverConfigured = Config.hasServerConfig()
|
||||
|
||||
// Determine which screen to show
|
||||
startDestination = if (!serverConfigured) {
|
||||
"serverConfig"
|
||||
} else {
|
||||
"login"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// On error, start with server config
|
||||
startDestination = "serverConfig"
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
Config.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,82 +65,73 @@ fun App() {
|
||||
CompositionLocalProvider(
|
||||
LocalNavController provides navController
|
||||
) {
|
||||
if (startDestination == null) {
|
||||
Box(
|
||||
modifier = androidx.compose.ui.Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination!!,
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
Start,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutOfContainer(
|
||||
Start,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideIntoContainer(
|
||||
End,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutOfContainer(
|
||||
End,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
}
|
||||
) {
|
||||
composable("serverConfig") {
|
||||
ServerConfigScreen()
|
||||
}
|
||||
|
||||
composable("login") {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
navController.navigate("chat") {
|
||||
popUpTo("login") { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateToRegister = { navController.navigate("register") }
|
||||
)
|
||||
}
|
||||
|
||||
composable("register") {
|
||||
RegisterScreen(
|
||||
onRegistered = { navController.navigate("login") }
|
||||
)
|
||||
}
|
||||
|
||||
composable("chat") {
|
||||
MainScreen(
|
||||
onLogout = {
|
||||
navController.navigate("login") {
|
||||
popUpTo("chat") { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("chats/publicChat") {
|
||||
PublicChatScreen()
|
||||
}
|
||||
|
||||
composable("about") {
|
||||
AboutScreen()
|
||||
}
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "login",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
Start,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutOfContainer(
|
||||
Start,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideIntoContainer(
|
||||
End,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutOfContainer(
|
||||
End,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
composable("serverConfig") {
|
||||
ServerConfigScreen()
|
||||
}
|
||||
|
||||
composable("login") {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
navController.navigate("chat") {
|
||||
popUpTo("login") { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateToRegister = { navController.navigate("register") }
|
||||
)
|
||||
}
|
||||
|
||||
composable("register") {
|
||||
RegisterScreen(
|
||||
onRegistered = { navController.navigate("login") }
|
||||
)
|
||||
}
|
||||
|
||||
composable("chat") {
|
||||
MainScreen(
|
||||
onLogout = {
|
||||
navController.navigate("login") {
|
||||
popUpTo("chat") { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("chats/publicChat") {
|
||||
PublicChatScreen()
|
||||
}
|
||||
|
||||
composable("about") {
|
||||
AboutScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
package ru.fromchat.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Login
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -33,24 +43,73 @@ import ru.fromchat.Res
|
||||
import ru.fromchat.api.ApiClient
|
||||
import ru.fromchat.api.LoginRequest
|
||||
import ru.fromchat.api.apiRequest
|
||||
import ru.fromchat.change_server
|
||||
import ru.fromchat.error_unexpected
|
||||
import ru.fromchat.fill_all_fields
|
||||
import ru.fromchat.login
|
||||
import ru.fromchat.login_d
|
||||
import ru.fromchat.more
|
||||
import ru.fromchat.password
|
||||
import ru.fromchat.register_button
|
||||
import ru.fromchat.ui.LocalNavController
|
||||
import ru.fromchat.ui.RowHeader
|
||||
import ru.fromchat.username
|
||||
import ru.fromchat.welcome
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: () -> Unit,
|
||||
onNavigateToRegister: () -> Unit
|
||||
) {
|
||||
val errorUnexpected = stringResource(Res.string.error_unexpected)
|
||||
val navController = LocalNavController.current
|
||||
|
||||
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets.safeDrawing,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
actions = {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(Modifier.wrapContentSize(Alignment.TopEnd)) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
expanded = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(Res.string.more)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false } // Закрыть при нажатии вне меню
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(Res.string.change_server))
|
||||
},
|
||||
onClick = {
|
||||
expanded = false
|
||||
navController.navigate("serverConfig")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Filled.Storage,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var alert by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ru.fromchat.ui.chat
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlin.math.abs
|
||||
@@ -7,90 +8,23 @@ import kotlin.math.abs
|
||||
/**
|
||||
* Get gradient brush for own messages
|
||||
*/
|
||||
fun getMessageGradient(isDark: Boolean): Brush {
|
||||
return if (isDark) {
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF9333EA),
|
||||
Color(0xFF6366F1),
|
||||
Color(0xFF2F68C5)
|
||||
),
|
||||
start = androidx.compose.ui.geometry.Offset(0f, 0f),
|
||||
end = androidx.compose.ui.geometry.Offset(1000f, 1000f)
|
||||
)
|
||||
} else {
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFB794F6),
|
||||
Color(0xFF818CF8),
|
||||
Color(0xFF60A5FA)
|
||||
),
|
||||
start = androidx.compose.ui.geometry.Offset(0f, 0f),
|
||||
end = androidx.compose.ui.geometry.Offset(1000f, 1000f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get background gradient brushes for chat background
|
||||
*/
|
||||
fun getBackgroundGradients(isDark: Boolean): List<Brush> {
|
||||
return if (isDark) {
|
||||
fun getMessageGradient(isDark: Boolean) = Brush.linearGradient(
|
||||
colors = if (isDark) {
|
||||
listOf(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0x26DBA1F9),
|
||||
Color.Transparent
|
||||
),
|
||||
center = androidx.compose.ui.geometry.Offset(0.2f, 0.8f),
|
||||
radius = 500f
|
||||
),
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0x26F3B7BE),
|
||||
Color.Transparent
|
||||
),
|
||||
center = androidx.compose.ui.geometry.Offset(0.8f, 0.2f),
|
||||
radius = 500f
|
||||
),
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0x1AD0C1DA),
|
||||
Color.Transparent
|
||||
),
|
||||
center = androidx.compose.ui.geometry.Offset(0.4f, 0.4f),
|
||||
radius = 500f
|
||||
)
|
||||
Color(0xFF9333EA),
|
||||
Color(0xFF6366F1),
|
||||
Color(0xFF2F68C5)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0x15B794F6),
|
||||
Color.Transparent
|
||||
),
|
||||
center = androidx.compose.ui.geometry.Offset(0.2f, 0.8f),
|
||||
radius = 500f
|
||||
),
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0x15F3B7BE),
|
||||
Color.Transparent
|
||||
),
|
||||
center = androidx.compose.ui.geometry.Offset(0.8f, 0.2f),
|
||||
radius = 500f
|
||||
),
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0x0DD0C1DA),
|
||||
Color.Transparent
|
||||
),
|
||||
center = androidx.compose.ui.geometry.Offset(0.4f, 0.4f),
|
||||
radius = 500f
|
||||
)
|
||||
Color(0xFFB794F6),
|
||||
Color(0xFF818CF8),
|
||||
Color(0xFF60A5FA)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(1000f, 1000f)
|
||||
)
|
||||
|
||||
/**
|
||||
* Generate a consistent gradient from a name for avatar fallback
|
||||
@@ -102,21 +36,21 @@ fun generateGradientFromName(name: String): Brush {
|
||||
val b = abs((hash / 65536) % 256)
|
||||
|
||||
// Create two colors based on hash for gradient
|
||||
val color1 = Color(
|
||||
red = (r + 100).coerceIn(0, 255) / 255f,
|
||||
green = (g + 100).coerceIn(0, 255) / 255f,
|
||||
blue = (b + 100).coerceIn(0, 255) / 255f
|
||||
)
|
||||
val color2 = Color(
|
||||
red = (r + 50).coerceIn(0, 255) / 255f,
|
||||
green = (g + 50).coerceIn(0, 255) / 255f,
|
||||
blue = (b + 50).coerceIn(0, 255) / 255f
|
||||
)
|
||||
|
||||
return Brush.linearGradient(
|
||||
colors = listOf(color1, color2),
|
||||
start = androidx.compose.ui.geometry.Offset(0f, 0f),
|
||||
end = androidx.compose.ui.geometry.Offset(100f, 100f)
|
||||
colors = listOf(
|
||||
Color(
|
||||
red = (r + 100).coerceIn(0, 255) / 255f,
|
||||
green = (g + 100).coerceIn(0, 255) / 255f,
|
||||
blue = (b + 100).coerceIn(0, 255) / 255f
|
||||
),
|
||||
Color(
|
||||
red = (r + 50).coerceIn(0, 255) / 255f,
|
||||
green = (g + 50).coerceIn(0, 255) / 255f,
|
||||
blue = (b + 50).coerceIn(0, 255) / 255f
|
||||
)
|
||||
),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(100f, 100f)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package ru.fromchat.ui.chat
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
@@ -20,15 +25,16 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -41,6 +47,7 @@ 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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -56,6 +63,78 @@ import ru.fromchat.Res
|
||||
import ru.fromchat.api.Message
|
||||
import ru.fromchat.message_placeholder
|
||||
|
||||
@Composable
|
||||
private fun <T> AnimatedPreviewBar(
|
||||
state: T?,
|
||||
content: @Composable (T) -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = state != null,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
var lastState by remember { mutableStateOf(state) }
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state != null) {
|
||||
lastState = state
|
||||
}
|
||||
}
|
||||
|
||||
if (state != null || lastState != null) {
|
||||
content(state ?: lastState!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviewBar(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 12.dp, top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun ChatInput(
|
||||
@@ -77,6 +156,7 @@ fun ChatInput(
|
||||
if (text.isNotBlank()) {
|
||||
typingJob?.cancel()
|
||||
typingHandler.sendTyping()
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
typingJob = scope.launch {
|
||||
delay(3000) // 3 seconds
|
||||
typingHandler.stopTyping()
|
||||
@@ -90,30 +170,6 @@ fun ChatInput(
|
||||
Column(
|
||||
Modifier.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
) {
|
||||
// Reply preview
|
||||
replyTo?.let { reply ->
|
||||
ReplyPreviewBar(
|
||||
replyTo = reply,
|
||||
onClose = onClearReply,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.hazeEffect(hazeState, style = HazeMaterials.thick())
|
||||
)
|
||||
}
|
||||
|
||||
// Edit preview
|
||||
editingMessage?.let { edit ->
|
||||
EditPreviewBar(
|
||||
message = edit,
|
||||
onClose = onClearEdit,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.hazeEffect(hazeState, style = HazeMaterials.thick())
|
||||
)
|
||||
}
|
||||
|
||||
// Input field with blur
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -121,28 +177,46 @@ fun ChatInput(
|
||||
.background(Color.Transparent)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
Dp.Hairline,
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||
shape
|
||||
)
|
||||
.clip(shape)
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thin()
|
||||
)
|
||||
) {
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
AnimatedPreviewBar(replyTo) { replyTo ->
|
||||
PreviewBar(
|
||||
icon = Icons.AutoMirrored.Filled.Reply,
|
||||
title = "Replying to ${replyTo.username}",
|
||||
subtitle = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "",
|
||||
onClose = { onClearReply() }
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedPreviewBar(editingMessage) { message ->
|
||||
PreviewBar(
|
||||
icon = Icons.Filled.Edit,
|
||||
title = "Editing message",
|
||||
subtitle = message.content.take(50) + if (message.content.length > 50) "..." else "",
|
||||
onClose = { onClearEdit() }
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = onTextChange,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.border(
|
||||
Dp.Hairline,
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||
shape
|
||||
)
|
||||
.clip(shape)
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thin()
|
||||
),
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(Res.string.message_placeholder),
|
||||
@@ -199,92 +273,4 @@ fun ChatInput(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReplyPreviewBar(
|
||||
replyTo: Message,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier, // hazeEffect moved to ChatInput where it's called
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = Color.Transparent
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Replying to ${replyTo.username}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditPreviewBar(
|
||||
message: Message,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier, // hazeEffect moved to ChatInput where it's called
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = Color.Transparent
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Editing message",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = message.content.take(50) + if (message.content.length > 50) "..." else "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,7 +279,10 @@ fun ChatScreen(
|
||||
replyTo = replyTo,
|
||||
editingMessage = editingMessage,
|
||||
onClearReply = { replyTo = null },
|
||||
onClearEdit = { editingMessage = null },
|
||||
onClearEdit = {
|
||||
editingMessage = null
|
||||
inputText = ""
|
||||
},
|
||||
hazeState = hazeState
|
||||
)
|
||||
}
|
||||
@@ -355,13 +358,17 @@ fun ChatScreen(
|
||||
}
|
||||
|
||||
// Context menu
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
MessageContextMenu(
|
||||
state = contextMenuState,
|
||||
isAuthor = contextMenuState.message?.user_id == currentUserId,
|
||||
onDismiss = { contextMenuState = contextMenuState.copy(isOpen = false) },
|
||||
onReply = { message ->
|
||||
replyTo = message
|
||||
editingMessage = null
|
||||
if (editingMessage != null) {
|
||||
editingMessage = null
|
||||
inputText = ""
|
||||
}
|
||||
},
|
||||
onEdit = { message ->
|
||||
editingMessage = message
|
||||
|
||||
@@ -188,20 +188,6 @@ fun SettingsTab(
|
||||
onClick = {
|
||||
// Logout and navigate to server config when server changes
|
||||
scope.launch {
|
||||
// Logout from current server
|
||||
try {
|
||||
ApiClient.token?.let { token ->
|
||||
ApiClient.logout(token)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore logout errors (server might be unreachable)
|
||||
}
|
||||
|
||||
// Clear API client state
|
||||
ApiClient.token = null
|
||||
ApiClient.user = null
|
||||
WebSocketManager.shutdown()
|
||||
|
||||
navController.navigate("serverConfig")
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,7 +52,6 @@ import ru.fromchat.https_enabled
|
||||
import ru.fromchat.save_continue
|
||||
import ru.fromchat.server_config_subtitle
|
||||
import ru.fromchat.server_config_title
|
||||
import ru.fromchat.server_url_hint
|
||||
import ru.fromchat.server_url_label
|
||||
import ru.fromchat.ui.LocalNavController
|
||||
|
||||
@@ -63,16 +62,16 @@ fun ServerConfigScreen() {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
// Load existing config if available
|
||||
var serverUrl by remember { mutableStateOf("") }
|
||||
var httpsEnabled by remember { mutableStateOf(false) }
|
||||
var serverUrl by remember { mutableStateOf("fromchat.ru") }
|
||||
var httpsEnabled by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val config = Config.serverConfig.value
|
||||
if (config != null) {
|
||||
serverUrl = config.serverUrl
|
||||
httpsEnabled = config.httpsEnabled
|
||||
Config.serverConfig.value?.let {
|
||||
serverUrl = it.serverUrl
|
||||
httpsEnabled = it.httpsEnabled
|
||||
}
|
||||
}
|
||||
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -82,11 +81,13 @@ fun ServerConfigScreen() {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navController::navigateUp) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(Res.string.back)
|
||||
)
|
||||
if (navController.currentBackStackEntry != null) {
|
||||
IconButton(onClick = navController::navigateUp) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(Res.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
@@ -122,7 +123,7 @@ fun ServerConfigScreen() {
|
||||
value = serverUrl,
|
||||
onValueChange = { serverUrl = it },
|
||||
label = { Text(stringResource(Res.string.server_url_label)) },
|
||||
placeholder = { Text(stringResource(Res.string.server_url_hint)) },
|
||||
placeholder = { Text("fromchat.ru") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
@@ -147,16 +148,13 @@ fun ServerConfigScreen() {
|
||||
onClick = {
|
||||
isLoading = true
|
||||
scope.launch {
|
||||
val config = ServerConfigData(serverUrl, httpsEnabled)
|
||||
Config.updateServerConfig(config)
|
||||
Config.updateServerConfig(ServerConfigData(serverUrl, httpsEnabled))
|
||||
|
||||
// Ensure we're logged out when server config changes
|
||||
try {
|
||||
runCatching {
|
||||
ApiClient.token?.let { token ->
|
||||
ApiClient.logout(token)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore logout errors
|
||||
}
|
||||
|
||||
// Clear API client state
|
||||
|
||||
+191
-35
@@ -1,46 +1,202 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 1. Setup
|
||||
# --- Настройка и окружение ---
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 2. Variables
|
||||
TAG=$(git describe --tags --abbrev=0)
|
||||
# --- Обработка аргументов ---
|
||||
IS_PRERELEASE=false
|
||||
|
||||
show_help() {
|
||||
echo "Usage: $0 [arguments]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " --pre Enable pre-release"
|
||||
echo " --help Show this message"
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--pre)
|
||||
IS_PRERELEASE=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "\033[0;31mError: Unknown argument '$arg'\033[0m"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Конфигурация цветов и оформления ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
BOLD='\033[1m'
|
||||
|
||||
# --- Функции логирования ---
|
||||
info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
||||
success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
error() { echo -e "${RED}✗${NC} $1"; }
|
||||
step() { echo -e "\n${CYAN}${BOLD}→${NC} ${BOLD}$1${NC}"; }
|
||||
substep() { echo -e " ${GREEN}•${NC} $1"; }
|
||||
|
||||
echo -e "${MAGENTA}${BOLD}🚀 FromChat KMP Release Pipeline${NC}"
|
||||
[[ "$IS_PRERELEASE" == true ]] && info "Prerelease mode enabled via --pre flag"
|
||||
|
||||
# --- Настройка памяти для предотвращения OutOfMemory ---
|
||||
export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx8g -Dkotlin.daemon.jvm.options=-Xmx8g"
|
||||
|
||||
# --- Переменные Git / Версии ---
|
||||
step "Metadata collection"
|
||||
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.1")
|
||||
# shellcheck disable=SC2207
|
||||
TAGS=($(git --no-pager tag --sort -v:refname | xargs))
|
||||
PREVTAG=${TAGS[1]}
|
||||
ASSET_PATH=./app/build/outputs/apk/release/app-release.apk
|
||||
DISPLAY_NAME=FromChat.$TAG.apk
|
||||
PREVTAG=${TAGS[1]:-$TAG}
|
||||
|
||||
# 3. Build release variant
|
||||
./gradlew assembleRelease
|
||||
substep "Current tag: ${YELLOW}$TAG${NC}"
|
||||
substep "Previous tag: ${YELLOW}$PREVTAG${NC}"
|
||||
|
||||
# 4. Move locally
|
||||
BUILD_NUMBER=$(echo "$TAG" | sed 's/[^0-9]//g')
|
||||
[[ -z "$BUILD_NUMBER" ]] && BUILD_NUMBER=1
|
||||
|
||||
# --- Сборка Android ---
|
||||
build_android() {
|
||||
step "Building Android Release"
|
||||
if ./gradlew :app:android:assembleRelease -PbuildNumber="$BUILD_NUMBER" -Dorg.gradle.jvmargs="-Xmx8g"; then
|
||||
APK_SRC=$(find . -name "*release.apk" | head -n 1)
|
||||
|
||||
if [[ -f "$APK_SRC" ]]; then
|
||||
mkdir -p releases
|
||||
DISPLAY_NAME="FromChat-$TAG-android.apk"
|
||||
cp "$APK_SRC" "releases/$DISPLAY_NAME"
|
||||
ANDROID_ASSET="releases/$DISPLAY_NAME"
|
||||
success "Android APK ready: ${CYAN}$DISPLAY_NAME${NC}"
|
||||
else
|
||||
error "Android APK not found after build"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
error "Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Сборка iOS ---
|
||||
build_ios() {
|
||||
step "Building iOS Release"
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
warning "iOS build requires macOS. Skipping."
|
||||
return
|
||||
fi
|
||||
|
||||
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
[[ ! -d "$DEVELOPER_DIR" ]] && export DEVELOPER_DIR=$(xcode-select -p)
|
||||
|
||||
IOS_PROJECT_DIR="app/ios"
|
||||
[[ ! -d "$IOS_PROJECT_DIR" ]] && IOS_PROJECT_DIR="iosApp"
|
||||
|
||||
substep "Setting iOS version to $BUILD_NUMBER..."
|
||||
PLIST_PATH=$(find "$IOS_PROJECT_DIR" -name "Info.plist" | head -n 1)
|
||||
if [[ -f "$PLIST_PATH" ]]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$PLIST_PATH" || true
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${TAG#v}" "$PLIST_PATH" || true
|
||||
fi
|
||||
|
||||
substep "Xcode Archiving (Unsigned)..."
|
||||
|
||||
mkdir -p build/ios
|
||||
|
||||
if xcodebuild -project "$IOS_PROJECT_DIR/iosApp.xcodeproj" \
|
||||
-scheme iOS \
|
||||
-configuration Release \
|
||||
-sdk iphoneos \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-derivedDataPath build/ios \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ENTITLEMENTS="" \
|
||||
build > build/ios/build.log 2>&1; then
|
||||
|
||||
success "Xcode build successful."
|
||||
else
|
||||
error "Xcode build failed. Check build/ios/build.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
substep "Packaging IPA for TrollStore..."
|
||||
mkdir -p build/ios/Payload
|
||||
APP_PATH=$(find build/ios -name "*.app" -type d | head -n 1)
|
||||
if [[ -n "$APP_PATH" ]]; then
|
||||
cp -r "$APP_PATH" build/ios/Payload/
|
||||
IPA_NAME="FromChat-$TAG-ios-unsigned.ipa"
|
||||
zip -r "releases/$IPA_NAME" build/ios/Payload > /dev/null
|
||||
rm -rf build/ios/Payload
|
||||
IOS_ASSET="releases/$IPA_NAME"
|
||||
success "iOS IPA ready: ${CYAN}$IPA_NAME${NC}"
|
||||
else
|
||||
error "Could not find .app bundle"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Публикация в GitHub ---
|
||||
publish_github() {
|
||||
step "GitHub Deployment"
|
||||
if ! command -v gh &> /dev/null; then
|
||||
warning "GitHub CLI (gh) not found. Skipping upload."
|
||||
return
|
||||
fi
|
||||
|
||||
substep "Pushing tags..."
|
||||
git push --tags > /dev/null 2>&1 || true
|
||||
|
||||
# Определяем флаг пререлиза на основе тега ИЛИ аргумента --pre
|
||||
prerelease_flag=""
|
||||
if [[ "$IS_PRERELEASE" == true ]] || [[ "$TAG" == v*-pre* ]]; then
|
||||
prerelease_flag="--prerelease"
|
||||
fi
|
||||
|
||||
# Собираем существующие файлы в массив
|
||||
ASSETS=()
|
||||
[[ -f "$ANDROID_ASSET" ]] && ASSETS+=("$ANDROID_ASSET")
|
||||
[[ -f "$IOS_ASSET" ]] && ASSETS+=("$IOS_ASSET")
|
||||
|
||||
if [ ${#ASSETS[@]} -eq 0 ]; then
|
||||
error "No assets found to upload. Check build logs."
|
||||
return
|
||||
fi
|
||||
|
||||
# Проверяем наличие релиза и создаем/обновляем
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
substep "Updating existing release ${YELLOW}$TAG${NC}..."
|
||||
[[ -n "$prerelease_flag" ]] && gh release edit "$TAG" "$prerelease_flag"
|
||||
gh release upload "$TAG" "${ASSETS[@]}" --clobber
|
||||
else
|
||||
substep "Creating new release ${YELLOW}$TAG${NC}..."
|
||||
gh release create "$TAG" \
|
||||
--generate-notes \
|
||||
--notes-start-tag "${PREVTAG:-$TAG}" \
|
||||
$prerelease_flag \
|
||||
"${ASSETS[@]}"
|
||||
fi
|
||||
success "Assets uploaded to GitHub Release"
|
||||
}
|
||||
|
||||
# --- Основной процесс ---
|
||||
mkdir -p releases
|
||||
mv "$ASSET_PATH" "releases/$DISPLAY_NAME"
|
||||
ASSET_PATH=./releases/$DISPLAY_NAME
|
||||
build_android
|
||||
build_ios
|
||||
publish_github
|
||||
|
||||
# 6. Release to GitHub
|
||||
prerelease_opt=$([[ "$TAG" != v*-pre* ]] || echo "--prerelease")
|
||||
|
||||
new_release() {
|
||||
gh release create \
|
||||
"$TAG" \
|
||||
--generate-notes \
|
||||
--notes-start-tag "$PREVTAG" \
|
||||
"$prerelease_opt" \
|
||||
"$ASSET_PATH"
|
||||
}
|
||||
|
||||
edit_existing() {
|
||||
gh release edit \
|
||||
"$TAG" \
|
||||
"$prerelease_opt"
|
||||
gh release upload \
|
||||
"$TAG" \
|
||||
--clobber \
|
||||
"$ASSET_PATH"
|
||||
}
|
||||
|
||||
git push --tags
|
||||
new_release || edit_existing
|
||||
echo -e "\n${GREEN}${BOLD}✨ Release $TAG completed successfully!${NC}"
|
||||
+15
-18
@@ -17,34 +17,31 @@ object ServerConfigStorage {
|
||||
private const val SERVER_URL_KEY = "server_url"
|
||||
private const val HTTPS_ENABLED_KEY = "https_enabled"
|
||||
|
||||
suspend fun getServerUrl(): String? {
|
||||
val url = storage.getString(SERVER_URL_KEY)
|
||||
return url.ifEmpty { null }
|
||||
}
|
||||
suspend fun getServerUrl() = storage.getString(SERVER_URL_KEY).ifEmpty { null }
|
||||
|
||||
suspend fun setServerUrl(url: String) {
|
||||
storage.putString(SERVER_URL_KEY, url)
|
||||
}
|
||||
suspend fun setServerUrl(url: String) = storage.putString(SERVER_URL_KEY, url)
|
||||
|
||||
suspend fun getHttpsEnabled(): Boolean? {
|
||||
return if (storage.contains(HTTPS_ENABLED_KEY)) {
|
||||
suspend fun getHttpsEnabled() =
|
||||
if (storage.contains(HTTPS_ENABLED_KEY))
|
||||
storage.getBoolean(HTTPS_ENABLED_KEY, true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else null
|
||||
|
||||
suspend fun setHttpsEnabled(enabled: Boolean) {
|
||||
storage.putBoolean(HTTPS_ENABLED_KEY, enabled)
|
||||
}
|
||||
|
||||
suspend fun hasConfiguration(): Boolean {
|
||||
return getServerUrl() != null
|
||||
}
|
||||
suspend fun hasConfiguration() = getServerUrl() != null && getHttpsEnabled() != null
|
||||
|
||||
suspend fun getConfig(): ServerConfigData {
|
||||
val url = getServerUrl() ?: throw IllegalStateException("Server URL not found in storage")
|
||||
val https = getHttpsEnabled() ?: true
|
||||
var url = getServerUrl()
|
||||
var https = getHttpsEnabled()
|
||||
|
||||
if (url == null || https == null) {
|
||||
url = "fromchat.ru"
|
||||
https = true
|
||||
saveConfig(ServerConfigData(url, https))
|
||||
}
|
||||
|
||||
return ServerConfigData(url, https)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user