3 Commits

6 changed files with 92 additions and 75 deletions
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.2.1</string> <string>0.2.2</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>021</string> <string>022</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
@@ -97,7 +97,7 @@ private fun PreviewBar(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, top = 8.dp), .padding(start = 12.dp, end = 6.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -188,13 +188,12 @@ fun ChatScreen(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
Column( Column(Modifier.fillMaxWidth()) {
horizontalAlignment = Alignment.CenterHorizontally
) {
Text( Text(
text = panelState.title, text = panelState.title,
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
AnimatedContent( AnimatedContent(
targetState = currentTypingUsers.isNotEmpty(), targetState = currentTypingUsers.isNotEmpty(),
transitionSpec = { transitionSpec = {
@@ -207,9 +206,6 @@ fun ChatScreen(
typingUsers = currentTypingUsers.map { it.username }, typingUsers = currentTypingUsers.map { it.username },
modifier = Modifier.padding(top = 2.dp) modifier = Modifier.padding(top = 2.dp)
) )
} else {
// Empty space to maintain height
Box(modifier = Modifier.height(0.dp))
} }
} }
} }
@@ -29,7 +29,7 @@ class PublicChatPanel(
scope.launch { scope.launch {
typingHandler.typingUsers.collect { users -> typingHandler.typingUsers.collect { users ->
Logger.d("PublicChatPanel", "Typing users updated in handler: ${users.map { it.username }}") Logger.d("PublicChatPanel", "Typing users updated in handler: ${users.map { it.username }}")
updateState { it.copy(typingUsers = users) } updateState { it.copy(typingUsers = users.filter { it.userId != currentUserId }) }
} }
} }
} }
@@ -58,7 +58,7 @@ class PublicChatPanel(
} }
setHasMoreMessages(false) // TODO: Implement has_more from API setHasMoreMessages(false) // TODO: Implement has_more from API
messagesLoaded = true messagesLoaded = true
} catch (e: Exception) { } catch (_: Exception) {
// Handle error // Handle error
} finally { } finally {
setLoading(false) setLoading(false)
@@ -84,7 +84,7 @@ class PublicChatPanel(
} }
} }
setHasMoreMessages(false) // TODO: Implement has_more from API setHasMoreMessages(false) // TODO: Implement has_more from API
} catch (e: Exception) { } catch (_: Exception) {
// Handle error // Handle error
} finally { } finally {
setLoadingMore(false) setLoadingMore(false)
@@ -4,9 +4,12 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -16,7 +19,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res import ru.fromchat.Res
@@ -63,67 +65,54 @@ private fun formatTypingText(typingUsers: List<String>): String {
@Composable @Composable
private fun TypingDots() { private fun TypingDots() {
val infiniteTransition = rememberInfiniteTransition(label = "typing_dots") val infiniteTransition = rememberInfiniteTransition(label = "typing_dots")
val duration = 1000
val initialScale = 1f
val targetScale = 1.3f
val dot1Alpha by infiniteTransition.animateFloat( // Функция-хелпер для создания анимации с задержкой
initialValue = 0.3f, @Composable
targetValue = 1f, fun animateDotScale(delay: Int) = infiniteTransition.animateFloat(
initialValue = initialScale,
targetValue = initialScale, // Возвращаемся в начало
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = keyframes { animation = keyframes {
durationMillis = 1400 durationMillis = duration
0.3f at 0 initialScale at delay // Начало подъема
1f at 200 targetScale at delay + 200 // Пик
0.3f at 400 initialScale at delay + 400 // Возврат
initialScale at duration // Удержание до конца цикла
} }
), ),
label = "dot1" label = "dot_scale"
) )
val dot2Alpha by infiniteTransition.animateFloat( val dot1Scale by animateDotScale(delay = 0)
initialValue = 0.3f, val dot2Scale by animateDotScale(delay = 200)
targetValue = 1f, val dot3Scale by animateDotScale(delay = 400)
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1400
0.3f at 200
1f at 400
0.3f at 600
}
),
label = "dot2"
)
val dot3Alpha by infiniteTransition.animateFloat( Row(
initialValue = 0.3f, verticalAlignment = Alignment.CenterVertically,
targetValue = 1f, horizontalArrangement = Arrangement.spacedBy(2.dp) // Вместо Spacer
animationSpec = infiniteRepeatable( ) {
animation = keyframes { Dot(scale = dot1Scale, maxScale = targetScale)
durationMillis = 1400 Dot(scale = dot2Scale, maxScale = targetScale)
0.3f at 400 Dot(scale = dot3Scale, maxScale = targetScale)
1f at 600
0.3f at 800
}
),
label = "dot3"
)
Row(verticalAlignment = Alignment.CenterVertically) {
Dot(alpha = dot1Alpha)
Spacer(modifier = Modifier.width(2.dp))
Dot(alpha = dot2Alpha)
Spacer(modifier = Modifier.width(2.dp))
Dot(alpha = dot3Alpha)
} }
} }
@Composable @Composable
private fun Dot(alpha: Float) { @Suppress("SameParameterValue")
private fun Dot(maxScale: Float, scale: Float) {
Box(
modifier = Modifier.size(4.dp * maxScale),
contentAlignment = Alignment.Center
) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.width(4.dp) .width(4.dp * scale)
.height(4.dp) .height(4.dp * scale),
.alpha(alpha),
shape = CircleShape, shape = CircleShape,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) {} ) {}
} }
}
+40 -8
View File
@@ -52,7 +52,6 @@ step() { echo -e "\n${CYAN}${BOLD}→${NC} ${BOLD}$1${NC}"; }
substep() { echo -e " ${GREEN}${NC} $1"; } substep() { echo -e " ${GREEN}${NC} $1"; }
echo -e "${MAGENTA}${BOLD}🚀 FromChat KMP Release Pipeline${NC}" echo -e "${MAGENTA}${BOLD}🚀 FromChat KMP Release Pipeline${NC}"
[[ "$IS_PRERELEASE" == true ]] && info "Prerelease mode enabled via --pre flag"
# --- Настройка памяти для предотвращения OutOfMemory --- # --- Настройка памяти для предотвращения OutOfMemory ---
export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx8g -Dkotlin.daemon.jvm.options=-Xmx8g" export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx8g -Dkotlin.daemon.jvm.options=-Xmx8g"
@@ -67,13 +66,14 @@ PREVTAG=${TAGS[1]:-$TAG}
substep "Current tag: ${YELLOW}$TAG${NC}" substep "Current tag: ${YELLOW}$TAG${NC}"
substep "Previous tag: ${YELLOW}$PREVTAG${NC}" substep "Previous tag: ${YELLOW}$PREVTAG${NC}"
# shellcheck disable=SC2001
BUILD_NUMBER=$(echo "$TAG" | sed 's/[^0-9]//g') BUILD_NUMBER=$(echo "$TAG" | sed 's/[^0-9]//g')
[[ -z "$BUILD_NUMBER" ]] && BUILD_NUMBER=1 [[ -z "$BUILD_NUMBER" ]] && BUILD_NUMBER=1
# --- Сборка Android --- # --- Сборка Android ---
build_android() { build_android() {
step "Building Android Release" step "Building Android Release"
if ./gradlew :app:android:assembleRelease -PbuildNumber="$BUILD_NUMBER" -Dorg.gradle.jvmargs="-Xmx8g"; then if ./gradlew :app:android:assembleRelease; then
APK_SRC=$(find . -name "*release.apk" | head -n 1) APK_SRC=$(find . -name "*release.apk" | head -n 1)
if [[ -f "$APK_SRC" ]]; then if [[ -f "$APK_SRC" ]]; then
@@ -101,6 +101,7 @@ build_ios() {
fi fi
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
# shellcheck disable=SC2155
[[ ! -d "$DEVELOPER_DIR" ]] && export DEVELOPER_DIR=$(xcode-select -p) [[ ! -d "$DEVELOPER_DIR" ]] && export DEVELOPER_DIR=$(xcode-select -p)
IOS_PROJECT_DIR="app/ios" IOS_PROJECT_DIR="app/ios"
@@ -150,6 +151,28 @@ build_ios() {
fi fi
} }
# --- Подготовка описания ---
prepare_description() {
DESC_FILE=".release_desc.md"
echo -e "<!--\nEnter the release description. Leave empty for no description.\n-->" > "$DESC_FILE"
# Открываем nano. Пользователь должен отредактировать файл.
nano "$DESC_FILE"
# Удаляем комментарии <!-- ... --> и лишние пробелы/пустые строки
# Используем perl для многострочного удаления комментариев
CLEAN_DESC=$(perl -0777 -pe 's/<!--.*?-->//gs' "$DESC_FILE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
rm -f "$DESC_FILE"
if [[ -n "$CLEAN_DESC" ]]; then
echo "$CLEAN_DESC" > .final_notes.md
NOTES_ARG=("-F" "./.final_notes.md")
else
NOTES_ARG=("--generate-notes")
fi
}
# --- Публикация в GitHub --- # --- Публикация в GitHub ---
publish_github() { publish_github() {
step "GitHub Deployment" step "GitHub Deployment"
@@ -161,36 +184,45 @@ publish_github() {
substep "Pushing tags..." substep "Pushing tags..."
git push --tags > /dev/null 2>&1 || true git push --tags > /dev/null 2>&1 || true
# Определяем флаг пререлиза на основе тега ИЛИ аргумента --pre
prerelease_flag="" prerelease_flag=""
if [[ "$IS_PRERELEASE" == true ]] || [[ "$TAG" == v*-pre* ]]; then if [[ "$IS_PRERELEASE" == true ]] || [[ "$TAG" == v*-pre* ]]; then
prerelease_flag="--prerelease" prerelease_flag="--prerelease"
fi fi
# Собираем существующие файлы в массив
ASSETS=() ASSETS=()
[[ -f "$ANDROID_ASSET" ]] && ASSETS+=("$ANDROID_ASSET") [[ -f "$ANDROID_ASSET" ]] && ASSETS+=("$ANDROID_ASSET")
[[ -f "$IOS_ASSET" ]] && ASSETS+=("$IOS_ASSET") [[ -f "$IOS_ASSET" ]] && ASSETS+=("$IOS_ASSET")
if [ ${#ASSETS[@]} -eq 0 ]; then if [ ${#ASSETS[@]} -eq 0 ]; then
error "No assets found to upload. Check build logs." error "No assets found to upload."
return return
fi fi
# Проверяем наличие релиза и создаем/обновляем # Подготавливаем описание перед созданием
prepare_description
if gh release view "$TAG" >/dev/null 2>&1; then if gh release view "$TAG" >/dev/null 2>&1; then
substep "Updating existing release ${YELLOW}$TAG${NC}..." substep "Updating existing release ${YELLOW}$TAG${NC}..."
[[ -n "$prerelease_flag" ]] && gh release edit "$TAG" "$prerelease_flag" [[ -n "$prerelease_flag" ]] && gh release edit "$TAG" "$prerelease_flag"
# Если есть кастомные ноты, обновляем их
if [[ -f .final_notes.md ]]; then
gh release edit "$TAG" -F ./.final_notes.md
fi
gh release upload "$TAG" "${ASSETS[@]}" --clobber gh release upload "$TAG" "${ASSETS[@]}" --clobber
else else
substep "Creating new release ${YELLOW}$TAG${NC}..." substep "Creating new release ${YELLOW}$TAG${NC}..."
# gh release create принимает либо --generate-notes, либо --notes-file
gh release create "$TAG" \ gh release create "$TAG" \
--generate-notes \ "${NOTES_ARG[@]}" \
--notes-start-tag "${PREVTAG:-$TAG}" \ --notes-start-tag "${PREVTAG:-$TAG}" \
$prerelease_flag \ $prerelease_flag \
"${ASSETS[@]}" "${ASSETS[@]}"
fi fi
success "Assets uploaded to GitHub Release"
rm -f .final_notes.md
success "Assets and notes uploaded to GitHub Release"
} }
# --- Основной процесс --- # --- Основной процесс ---