delete CloudKit and added BalanceFlutter
Android CI / Build and Test (push) Failing after 21m18s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
Android CI / Build and Test (push) Failing after 21m18s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
This commit is contained in:
1 parent
a715eca947
commit
2c278044bb
214 files changed
+13411
-2290
No files matched your search
@@ -94,13 +94,3 @@ export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
|
||||
Проект задаёт для Gradle 2 ГБ heap, 1 ГБ metaspace и не более двух параллельных workers. Эти параметры находятся в `gradle.properties` и применяются также при сборке из Android Studio.
|
||||
|
||||
Debug APK появится в `app/build/outputs/apk/debug/`.
|
||||
|
||||
## Документация проекта
|
||||
|
||||
- [`AGENTS.md`](AGENTS.md) — правила для AI-агентов и разработчиков.
|
||||
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — архитектура Android-клиента.
|
||||
- [`docs/DATA_MODEL.md`](docs/DATA_MODEL.md) — Room-модели и финансовые правила.
|
||||
- [`docs/SYNC.md`](docs/SYNC.md) — протокол синхронизации и безопасность сессии.
|
||||
- [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) — Android Studio, Gradle, тесты и release.
|
||||
- [`docs/PRODUCT.md`](docs/PRODUCT.md) — функциональная документация.
|
||||
|
||||
+5
-6
@@ -10,7 +10,6 @@ import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import androidx.core.content.edit
|
||||
|
||||
data class ServerSession(
|
||||
val accessToken: String,
|
||||
@@ -34,10 +33,10 @@ class SecureSessionStore(context: Context) {
|
||||
.toString().toByteArray()
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey())
|
||||
preferences.edit {
|
||||
putString("iv", Base64.encodeToString(cipher.iv, Base64.NO_WRAP))
|
||||
.putString("data", Base64.encodeToString(cipher.doFinal(source), Base64.NO_WRAP))
|
||||
}
|
||||
preferences.edit()
|
||||
.putString("iv", Base64.encodeToString(cipher.iv, Base64.NO_WRAP))
|
||||
.putString("data", Base64.encodeToString(cipher.doFinal(source), Base64.NO_WRAP))
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun load(): ServerSession? = runCatching {
|
||||
@@ -59,7 +58,7 @@ class SecureSessionStore(context: Context) {
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
preferences.edit { clear() }
|
||||
preferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
private fun secretKey(): SecretKey {
|
||||
|
||||
@@ -66,7 +66,7 @@ class ServerApi(
|
||||
return try {
|
||||
parseSession(post("v1/auth/refresh", JSONObject().put("refreshToken", session.refreshToken), null))
|
||||
.also { sessions.save(it) }
|
||||
} catch (_: Throwable) {
|
||||
} catch (error: Throwable) {
|
||||
sessions.clear()
|
||||
throw ServerException("Сессия истекла. Войдите снова.")
|
||||
}
|
||||
|
||||
+8
-19
@@ -4,27 +4,26 @@ import android.content.Context
|
||||
import java.net.URI
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import androidx.core.content.edit
|
||||
|
||||
class ServerPreferences(context: Context) {
|
||||
private val values = context.getSharedPreferences("balance_settings", Context.MODE_PRIVATE)
|
||||
|
||||
var serverUrl: String
|
||||
get() = values.getString("server_url", "") ?: ""
|
||||
set(value) { values.edit { putString("server_url", value) } }
|
||||
set(value) { values.edit().putString("server_url", value).apply() }
|
||||
var email: String
|
||||
get() = values.getString("server_email", "") ?: ""
|
||||
set(value) { values.edit { putString("server_email", value) } }
|
||||
set(value) { values.edit().putString("server_email", value).apply() }
|
||||
var currency: String
|
||||
get() = values.getString("currency", "RUB") ?: "RUB"
|
||||
set(value) { values.edit { putString("currency", value) } }
|
||||
set(value) { values.edit().putString("currency", value).apply() }
|
||||
var theme: String
|
||||
get() = values.getString("theme", "system") ?: "system"
|
||||
set(value) { values.edit { putString("theme", value) } }
|
||||
set(value) { values.edit().putString("theme", value).apply() }
|
||||
|
||||
val deviceId: String
|
||||
get() = values.getString("device_id", null) ?: UUID.randomUUID().toString().also {
|
||||
values.edit { putString("device_id", it) }
|
||||
values.edit().putString("device_id", it).apply()
|
||||
}
|
||||
|
||||
fun normalizeServer(input: String): String {
|
||||
@@ -46,21 +45,11 @@ class ServerPreferences(context: Context) {
|
||||
}
|
||||
|
||||
fun cursor(scope: String): Long = values.getLong("cursor_${hash(scope)}", 0).coerceAtLeast(0)
|
||||
fun setCursor(scope: String, cursor: Long) = values.edit {
|
||||
putLong(
|
||||
"cursor_${hash(scope)}",
|
||||
cursor
|
||||
)
|
||||
}
|
||||
fun setCursor(scope: String, cursor: Long) = values.edit().putLong("cursor_${hash(scope)}", cursor).apply()
|
||||
fun lastPush(scope: String): Long = values.getLong("push_${hash(scope)}", 0)
|
||||
fun setLastPush(scope: String, value: Long) = values.edit {
|
||||
putLong(
|
||||
"push_${hash(scope)}",
|
||||
value
|
||||
)
|
||||
}
|
||||
fun setLastPush(scope: String, value: Long) = values.edit().putLong("push_${hash(scope)}", value).apply()
|
||||
fun resetSync(scope: String) {
|
||||
values.edit { remove("cursor_${hash(scope)}").remove("push_${hash(scope)}") }
|
||||
values.edit().remove("cursor_${hash(scope)}").remove("push_${hash(scope)}").apply()
|
||||
}
|
||||
|
||||
private fun hash(value: String): String = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
@@ -32,8 +32,8 @@ class SyncRepository(
|
||||
val status: StateFlow<SyncStatus> = mutableStatus
|
||||
val session: StateFlow<ServerSession?> = mutableSession
|
||||
|
||||
// fun serverUrl(): String = preferences.serverUrl
|
||||
// fun savedEmail(): String = preferences.email
|
||||
fun serverUrl(): String = preferences.serverUrl
|
||||
fun savedEmail(): String = preferences.email
|
||||
|
||||
suspend fun changeServer(value: String): String {
|
||||
return try {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -12,9 +12,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.DirectionsRun
|
||||
import androidx.compose.material.icons.automirrored.filled.DirectionsWalk
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.filled.AccountBalanceWallet
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.AutoAwesome
|
||||
@@ -29,9 +26,11 @@ import androidx.compose.material.icons.filled.Coffee
|
||||
import androidx.compose.material.icons.filled.Construction
|
||||
import androidx.compose.material.icons.filled.CreditCard
|
||||
import androidx.compose.material.icons.filled.DirectionsCar
|
||||
import androidx.compose.material.icons.filled.DirectionsRun
|
||||
import androidx.compose.material.icons.filled.DirectionsWalk
|
||||
import androidx.compose.material.icons.filled.Eco
|
||||
import androidx.compose.material.icons.filled.Fastfood
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.Fastfood
|
||||
import androidx.compose.material.icons.filled.FitnessCenter
|
||||
import androidx.compose.material.icons.filled.Flight
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
@@ -43,6 +42,7 @@ import androidx.compose.material.icons.filled.LocalGasStation
|
||||
import androidx.compose.material.icons.filled.LocalGroceryStore
|
||||
import androidx.compose.material.icons.filled.MedicalServices
|
||||
import androidx.compose.material.icons.filled.Medication
|
||||
import androidx.compose.material.icons.filled.MenuBook
|
||||
import androidx.compose.material.icons.filled.MoreHoriz
|
||||
import androidx.compose.material.icons.filled.Movie
|
||||
import androidx.compose.material.icons.filled.MusicNote
|
||||
@@ -75,13 +75,10 @@ import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.balanceandroid.data.CategoryDesign
|
||||
import com.example.balanceandroid.data.CategoryTotal
|
||||
import com.example.balanceandroid.ui.theme.BalanceGreen
|
||||
import com.example.balanceandroid.ui.theme.BalanceTheme
|
||||
import java.text.NumberFormat
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
@@ -151,12 +148,12 @@ fun iconFor(name: String): ImageVector = when (name) {
|
||||
"printer.fill" -> Icons.Default.Print
|
||||
"pawprint.fill" -> Icons.Default.Pets
|
||||
"leaf.fill" -> Icons.Default.Eco
|
||||
"figure.walk" -> Icons.AutoMirrored.Filled.DirectionsWalk
|
||||
"figure.run" -> Icons.AutoMirrored.Filled.DirectionsRun
|
||||
"figure.walk" -> Icons.Default.DirectionsWalk
|
||||
"figure.run" -> Icons.Default.DirectionsRun
|
||||
"dumbbell.fill" -> Icons.Default.FitnessCenter
|
||||
"cross.case.fill", "stethoscope" -> Icons.Default.MedicalServices
|
||||
"pills.fill" -> Icons.Default.Medication
|
||||
"book.fill" -> Icons.AutoMirrored.Filled.MenuBook
|
||||
"book.fill" -> Icons.Default.MenuBook
|
||||
"school", "graduationcap.fill" -> Icons.Default.School
|
||||
"laptopcomputer" -> Icons.Default.Laptop
|
||||
"briefcase.fill" -> Icons.Default.Work
|
||||
@@ -230,34 +227,3 @@ fun periodBounds(months: Int, now: Long = System.currentTimeMillis()): Pair<Long
|
||||
val end = currentStart.plusMonths(1)
|
||||
return start.toInstant().toEpochMilli() to end.toInstant().toEpochMilli()
|
||||
}
|
||||
|
||||
@Preview(name = "Metric Card", showBackground = true)
|
||||
@Composable
|
||||
fun MetricCardPreview() {
|
||||
BalanceTheme(theme = "dark") {
|
||||
MetricCard("Доходы", "120 000 ₽", BalanceGreen)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Category Badge", showBackground = true)
|
||||
@Composable
|
||||
fun CategoryBadgePreview() {
|
||||
BalanceTheme(theme = "dark") {
|
||||
CategoryBadge(CategoryDesign("Продукты", "cart.fill", "🛒", "orange", "expense"))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Spending Bars", showBackground = true)
|
||||
@Composable
|
||||
fun SpendingBarsPreview() {
|
||||
BalanceTheme(theme = "dark") {
|
||||
SpendingBars(
|
||||
listOf(
|
||||
CategoryTotal("Продукты", "cart.fill", "🛒", "orange", 12500.0),
|
||||
CategoryTotal("Кафе", "fork.knife", "☕", "indigo", 4200.0),
|
||||
CategoryTotal("Транспорт", "car.fill", "🚗", "purple", 3100.0),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -54,14 +53,14 @@ import java.time.format.DateTimeFormatter
|
||||
@Composable
|
||||
fun TransactionEditorDialog(
|
||||
model: MainViewModel,
|
||||
// state: FinanceUiState,
|
||||
state: FinanceUiState,
|
||||
existing: TransactionEntity?,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var amount by remember(existing?.id) { mutableStateOf(existing?.amount?.toString().orEmpty()) }
|
||||
var kind by remember(existing?.id) { mutableStateOf(existing?.kindRawValue ?: "expense") }
|
||||
var note by remember(existing?.id) { mutableStateOf(existing?.note.orEmpty()) }
|
||||
var date by remember(existing?.id) { mutableLongStateOf(existing?.date ?: System.currentTimeMillis()) }
|
||||
var date by remember(existing?.id) { mutableStateOf(existing?.date ?: System.currentTimeMillis()) }
|
||||
val categories = model.categories(kind)
|
||||
var categoryName by remember(existing?.id) { mutableStateOf(existing?.categoryName ?: categories.first().name) }
|
||||
val selected = categories.firstOrNull { it.name == categoryName } ?: categories.first()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("com.android.application") version "9.2.1" apply false
|
||||
id("com.android.application") version "8.13.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.21" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" apply false
|
||||
id("org.jetbrains.kotlin.kapt") version "2.3.21" apply false
|
||||
|
||||
@@ -6,13 +6,3 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8
|
||||
kotlin.daemon.jvmargs=-Xmx1024m -XX:MaxMetaspaceSize=512m
|
||||
org.gradle.workers.max=2
|
||||
org.gradle.parallel=false
|
||||
android.defaults.buildfeatures.resvalues=true
|
||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||
android.enableAppCompileTimeRClass=false
|
||||
android.usesSdkInManifest.disallowed=false
|
||||
android.uniquePackageNames=false
|
||||
android.dependency.useConstraints=true
|
||||
android.r8.strictFullModeForKeepRules=false
|
||||
android.r8.optimizedResourceShrinking=false
|
||||
android.builtInKotlin=false
|
||||
android.newDsl=false
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Reference in new issue
Block a user