Проект пересобран без модуля desktopMain
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
*.iml
|
||||
.kotlin
|
||||
.gradle
|
||||
**/build/
|
||||
xcuserdata
|
||||
!src/**/build/
|
||||
local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcodeproj/project.xcworkspace/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
@@ -0,0 +1,25 @@
|
||||
# Помощник пчеловода
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Конфигурация
|
||||
|
||||
Путь до файла: `/composeApp/src/commonMain/kotlin/ru/cit71/bee_frontend/core/Config.kt`
|
||||
|
||||
```kotlin
|
||||
data object Config {
|
||||
const val DEBUG = false
|
||||
private const val TELEGRAM_INIT_DATA = "YOUR-TELEGRAM-INIT-DATA"
|
||||
}
|
||||
```
|
||||
|
||||
## Запуск и компиляция
|
||||
|
||||
| Задача | Команда |
|
||||
|--------------|-------------------------------------------|
|
||||
| desktop Run | `:composeApp:run` |
|
||||
| wasmJs Run | `:composeApp:wasmJsBrowserDevelopmentRun` |
|
||||
| wasmJs Build | `:composeApp:wasmJsBrowserDistribution` |
|
||||
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
// this is necessary to avoid the plugins to be loaded multiple times
|
||||
// in each subproject's classloader
|
||||
alias(libs.plugins.composeMultiplatform) apply false
|
||||
alias(libs.plugins.composeCompiler) apply false
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
|
||||
|
||||
plugins {
|
||||
// Initial
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
|
||||
// Custom
|
||||
kotlin("plugin.serialization") version "2.2.10"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
binaries.executable()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// Initial
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||
|
||||
// Custom
|
||||
//// Ktor
|
||||
implementation(libs.ktor.clientCore)
|
||||
implementation(libs.ktor.clientContentNegotiation)
|
||||
implementation(libs.ktor.clientLogging)
|
||||
implementation(libs.ktor.serializationKotlinxJson)
|
||||
|
||||
//// Coil
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.networkKtor)
|
||||
|
||||
//// Navigation
|
||||
implementation(libs.navigation.compose)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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="#FFFFFF" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
|
||||
</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="#FFFFFF" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
|
||||
</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="#FFFFFF" android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
|
||||
|
||||
</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="#FFFFFF" android:pathData="M2,15.5v2h20v-2L2,15.5zM2,10.5v2h20v-2L2,10.5zM2,5.5v2h20v-2L2,5.5z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M11,7h2v2h-2V7zM11,11h2v6h-2V11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<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="#FFFFFF" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V20h9.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l1.14,-1.01l-1,-1.73l-1.45,0.49c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63L18,11h-2l-0.3,1.49c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.45,-0.49l-1,1.73l1.14,1.01c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-1.14,1.01l1,1.73l1.45,-0.49c0.32,0.27 0.68,0.48 1.08,0.63L16,21h2l0.3,-1.49c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.45,0.49l1,-1.73l-1.14,-1.01C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
|
||||
|
||||
</vector>
|
||||
+5
@@ -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="#FFFFFF" android:pathData="M7.58,4.08L6.15,2.65C3.75,4.48 2.17,7.3 2.03,10.5h2c0.15,-2.65 1.51,-4.97 3.55,-6.42zM19.97,10.5h2c-0.15,-3.2 -1.73,-6.02 -4.12,-7.85l-1.42,1.43c2.02,1.45 3.39,3.77 3.54,6.42zM18,11c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2v-5zM12,22c0.14,0 0.27,-0.01 0.4,-0.04 0.65,-0.14 1.18,-0.58 1.44,-1.18 0.1,-0.24 0.15,-0.5 0.15,-0.78h-4c0.01,1.1 0.9,2 2.01,2z"/>
|
||||
|
||||
</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="#FFFFFF" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
|
||||
</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="#FFFFFF" android:pathData="M13,8c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8s1.79,4 4,4S13,10.21 13,8zM15,10v2h3v3h2v-3h3v-2h-3V7h-2v3H15zM1,18v2h16v-2c0,-2.66 -5.33,-4 -8,-4S1,15.34 1,18z"/>
|
||||
|
||||
</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="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,520Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M480,640L280,440L336,382L440,486L440,160L520,160L520,486L624,382L680,440L480,640ZM240,800Q207,800 183.5,776.5Q160,753 160,720L160,600L240,600L240,720Q240,720 240,720Q240,720 240,720L720,720Q720,720 720,720Q720,720 720,720L720,600L800,600L800,720Q800,753 776.5,776.5Q753,800 720,800L240,800Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M11.71,15.29l2.59,-2.59c0.39,-0.39 0.39,-1.02 0,-1.41L11.71,8.7c-0.63,-0.62 -1.71,-0.18 -1.71,0.71v5.17c0,0.9 1.08,1.34 1.71,0.71z"/>
|
||||
|
||||
</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="#ffffff" android:pathData="M9,16.17L5.53,12.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41l4.18,4.18c0.39,0.39 1.02,0.39 1.41,0L20.29,7.71c0.39,-0.39 0.39,-1.02 0,-1.41 -0.39,-0.39 -1.02,-0.39 -1.41,0L9,16.17z"/>
|
||||
|
||||
</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="#ffffff" android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z"/>
|
||||
|
||||
</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="#ffffff" android:pathData="M15,20H5V7c0,-0.55 -0.45,-1 -1,-1h0C3.45,6 3,6.45 3,7v13c0,1.1 0.9,2 2,2h10c0.55,0 1,-0.45 1,-1v0C16,20.45 15.55,20 15,20zM20,16V4c0,-1.1 -0.9,-2 -2,-2H9C7.9,2 7,2.9 7,4v12c0,1.1 0.9,2 2,2h9C19.1,18 20,17.1 20,16zM18,16H9V4h9V16z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<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="#ffffff" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V19c0,0.55 0.45,1 1,1h8.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l0.84,-0.73c0.18,-0.16 0.22,-0.42 0.1,-0.63l-0.59,-1.02c-0.12,-0.21 -0.37,-0.3 -0.59,-0.22l-1.06,0.36c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63l-0.22,-1.09c-0.05,-0.23 -0.25,-0.4 -0.49,-0.4h-1.18c-0.24,0 -0.44,0.17 -0.49,0.4l-0.22,1.09c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.06,-0.36c-0.23,-0.08 -0.47,0.02 -0.59,0.22l-0.59,1.02c-0.12,0.21 -0.08,0.47 0.1,0.63l0.84,0.73c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-0.84,0.73c-0.18,0.16 -0.22,0.42 -0.1,0.63l0.59,1.02c0.12,0.21 0.37,0.3 0.59,0.22l1.06,-0.36c0.32,0.27 0.68,0.48 1.08,0.63l0.22,1.09c0.05,0.23 0.25,0.4 0.49,0.4h1.18c0.24,0 0.44,-0.17 0.49,-0.4l0.22,-1.09c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.06,0.36c0.23,0.08 0.47,-0.02 0.59,-0.22l0.59,-1.02c0.12,-0.21 0.08,-0.47 -0.1,-0.63l-0.84,-0.73C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
|
||||
|
||||
</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="#ffffff" android:pathData="M19.5,12c0,-0.23 -0.01,-0.45 -0.03,-0.68l1.86,-1.41c0.4,-0.3 0.51,-0.86 0.26,-1.3l-1.87,-3.23c-0.25,-0.44 -0.79,-0.62 -1.25,-0.42l-2.15,0.91c-0.37,-0.26 -0.76,-0.49 -1.17,-0.68l-0.29,-2.31C14.8,2.38 14.37,2 13.87,2h-3.73C9.63,2 9.2,2.38 9.14,2.88L8.85,5.19c-0.41,0.19 -0.8,0.42 -1.17,0.68L5.53,4.96c-0.46,-0.2 -1,-0.02 -1.25,0.42L2.41,8.62c-0.25,0.44 -0.14,0.99 0.26,1.3l1.86,1.41C4.51,11.55 4.5,11.77 4.5,12s0.01,0.45 0.03,0.68l-1.86,1.41c-0.4,0.3 -0.51,0.86 -0.26,1.3l1.87,3.23c0.25,0.44 0.79,0.62 1.25,0.42l2.15,-0.91c0.37,0.26 0.76,0.49 1.17,0.68l0.29,2.31C9.2,21.62 9.63,22 10.13,22h3.73c0.5,0 0.93,-0.38 0.99,-0.88l0.29,-2.31c0.41,-0.19 0.8,-0.42 1.17,-0.68l2.15,0.91c0.46,0.2 1,0.02 1.25,-0.42l1.87,-3.23c0.25,-0.44 0.14,-0.99 -0.26,-1.3l-1.86,-1.41C19.49,12.45 19.5,12.23 19.5,12zM12.04,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5s3.5,1.57 3.5,3.5S13.97,15.5 12.04,15.5z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520ZM120,840Q87,840 63.5,816.5Q40,793 40,760L40,280Q40,247 63.5,223.5Q87,200 120,200L246,200L296,146Q307,134 322.5,127Q338,120 355,120L520,120Q537,120 548.5,131.5Q560,143 560,160L560,160Q560,177 548.5,188.5Q537,200 520,200L355,200L282,280L120,280Q120,280 120,280Q120,280 120,280L120,760Q120,760 120,760Q120,760 120,760L760,760Q760,760 760,760Q760,760 760,760L760,440Q760,423 771.5,411.5Q783,400 800,400L800,400Q817,400 828.5,411.5Q840,423 840,440L840,760Q840,793 816.5,816.5Q793,840 760,840L120,840ZM760,200L720,200Q703,200 691.5,188.5Q680,177 680,160Q680,143 691.5,131.5Q703,120 720,120L760,120L760,80Q760,63 771.5,51.5Q783,40 800,40Q817,40 828.5,51.5Q840,63 840,80L840,120L880,120Q897,120 908.5,131.5Q920,143 920,160Q920,177 908.5,188.5Q897,200 880,200L840,200L840,240Q840,257 828.5,268.5Q817,280 800,280Q783,280 771.5,268.5Q760,257 760,240L760,200ZM440,700Q515,700 567.5,647.5Q620,595 620,520Q620,445 567.5,392.5Q515,340 440,340Q365,340 312.5,392.5Q260,445 260,520Q260,595 312.5,647.5Q365,700 440,700ZM440,620Q398,620 369,591Q340,562 340,520Q340,478 369,449Q398,420 440,420Q482,420 511,449Q540,478 540,520Q540,562 511,591Q482,620 440,620Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,330 @@
|
||||
package ru.cit71.pchelovod71
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.baseline_content_paste_24
|
||||
import frontend.composeapp.generated.resources.baseline_person_24
|
||||
import frontend.composeapp.generated.resources.baseline_person_add_alt_1_24
|
||||
import frontend.composeapp.generated.resources.round_settings_24
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffold
|
||||
import ru.cit71.pchelovod71.ui.screens.RegistrationRequestsScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.RegistrationUserScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.RejectRegistrationUserScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.SettingsScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.UserRequestsScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.UsersScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.WaitRegistrationUserScreen
|
||||
|
||||
interface Screen {
|
||||
val route: String
|
||||
val title: String
|
||||
val icon: DrawableResource
|
||||
}
|
||||
|
||||
enum class AdminScreen(
|
||||
override val route: String,
|
||||
override val title: String,
|
||||
override val icon: DrawableResource,
|
||||
) : Screen {
|
||||
RegistrationRequests(
|
||||
route = "registration-requests",
|
||||
title = "Запросы на регистрацию",
|
||||
icon = Res.drawable.baseline_person_add_alt_1_24,
|
||||
),
|
||||
Users(
|
||||
route = "users",
|
||||
title = "Пользователи",
|
||||
icon = Res.drawable.baseline_person_24,
|
||||
),
|
||||
UserRequests(
|
||||
route = "user-requests",
|
||||
title = "Обращения пользователей",
|
||||
icon = Res.drawable.baseline_content_paste_24,
|
||||
),
|
||||
}
|
||||
|
||||
enum class UserScreen(
|
||||
override val route: String,
|
||||
override val title: String,
|
||||
override val icon: DrawableResource,
|
||||
) : Screen {
|
||||
MyRequests(
|
||||
route = "my-requests",
|
||||
title = "Мои обращения",
|
||||
icon = Res.drawable.baseline_content_paste_24,
|
||||
),
|
||||
Settings(
|
||||
route = "settings",
|
||||
title = "Настройки",
|
||||
icon = Res.drawable.round_settings_24,
|
||||
),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AdminApp(
|
||||
paddingValues: PaddingValues,
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
val navHostController = rememberNavController()
|
||||
|
||||
MainScaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry = navHostController.currentBackStackEntryAsState()
|
||||
|
||||
AdminScreen.entries.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(screen.icon),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = screen.title,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
selected = navBackStackEntry.value?.destination?.hierarchy?.any {
|
||||
it.route == screen.route
|
||||
} == true,
|
||||
onClick = {
|
||||
navHostController.navigate(screen.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
) { mainScaffoldParams ->
|
||||
NavHost(
|
||||
navController = navHostController,
|
||||
startDestination = AdminScreen.UserRequests.route,
|
||||
) {
|
||||
composable(AdminScreen.RegistrationRequests.route) {
|
||||
RegistrationRequestsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AdminScreen.Users.route) {
|
||||
UsersScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AdminScreen.UserRequests.route) {
|
||||
UserRequestsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserApp(
|
||||
paddingValues: PaddingValues,
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
val navHostController = rememberNavController()
|
||||
|
||||
MainScaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry = navHostController.currentBackStackEntryAsState()
|
||||
|
||||
UserScreen.entries.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(screen.icon),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = screen.title,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
selected = navBackStackEntry.value?.destination?.hierarchy?.any {
|
||||
it.route == screen.route
|
||||
} == true,
|
||||
onClick = {
|
||||
navHostController.navigate(screen.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
) { mainScaffoldParams ->
|
||||
NavHost(
|
||||
navController = navHostController,
|
||||
startDestination = UserScreen.MyRequests.route,
|
||||
) {
|
||||
composable(UserScreen.MyRequests.route) {
|
||||
UserRequestsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
|
||||
composable(UserScreen.Settings.route) {
|
||||
SettingsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun App() {
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = darkColorScheme(),
|
||||
) {
|
||||
MainScaffold { mainScaffoldParams ->
|
||||
suspend fun reloadUserDomain() {
|
||||
Api.Users.auth(
|
||||
authParams = Models.AuthParams.DTO.Send(
|
||||
initData = Api.telegramInitData,
|
||||
),
|
||||
onSuccess = { user ->
|
||||
userDomain = user.toDomain()
|
||||
},
|
||||
onError = {},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.telegramInitData = Platform.getTelegramInitData()
|
||||
reloadUserDomain()
|
||||
}
|
||||
}
|
||||
|
||||
when (userDomain) {
|
||||
null -> {
|
||||
var telegramId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
suspend fun reloadTelegramId() {
|
||||
Api.Users.getTelegramId(
|
||||
authParams = Models.AuthParams.DTO.Send(
|
||||
initData = Platform.getTelegramInitData(),
|
||||
),
|
||||
onSuccess = {
|
||||
telegramId = it.telegramId
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось авторизоваться с помощью учетной записи Telegram"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
when (val bindUuid = Platform.getBindUuid()) {
|
||||
null -> {
|
||||
reloadTelegramId()
|
||||
}
|
||||
else -> {
|
||||
Api.Users.bind(
|
||||
uuid = bindUuid,
|
||||
onSuccess = {
|
||||
reloadUserDomain()
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Учетная запись Telegram успешно привязана"
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
reloadTelegramId()
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось привязать учетную запись Telegram"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
telegramId?.let {
|
||||
RegistrationUserScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
telegramId = it,
|
||||
reloadUserDomain = ::reloadUserDomain,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
userDomain?.let { userDomain ->
|
||||
when (userDomain.deleted) {
|
||||
true -> {
|
||||
RejectRegistrationUserScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
false -> {
|
||||
when (userDomain.accepted) {
|
||||
true -> {
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> {
|
||||
AdminApp(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
Models.Role.User -> {
|
||||
UserApp(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
WaitRegistrationUserScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.DefaultRequest
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.patch
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.clone
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import ru.cit71.pchelovod71.core.Platform.base64Encode
|
||||
|
||||
object Api {
|
||||
lateinit var telegramInitData: String
|
||||
|
||||
private val httpClient = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
|
||||
install(DefaultRequest) {
|
||||
header("Authorization", "Bearer ${base64Encode(telegramInitData)}")
|
||||
}
|
||||
|
||||
if (Config.DEBUG)
|
||||
install(Logging) {
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
println(message)
|
||||
}
|
||||
}
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
}
|
||||
|
||||
private val apiUrlBuilder = Config.apiUrlBuilder
|
||||
|
||||
object Users {
|
||||
suspend fun getTelegramId(
|
||||
authParams: Models.AuthParams.DTO.Send,
|
||||
onSuccess: suspend (Models.AuthParams.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("auth", "telegram")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(authParams)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun auth(
|
||||
authParams: Models.AuthParams.DTO.Send,
|
||||
onSuccess: suspend (Models.User.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("auth", "user")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(authParams)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(
|
||||
verified: Boolean? = null,
|
||||
deleted: Boolean? = null,
|
||||
onSuccess: suspend (List<Models.User.DTO.Get>) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "")
|
||||
verified?.let {
|
||||
parameters.append("verified", "$it")
|
||||
}
|
||||
deleted?.let {
|
||||
parameters.append("deleted_status", "$it")
|
||||
}
|
||||
}.build()
|
||||
val response = httpClient.get(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
user: Models.User.DTO.Create,
|
||||
onSuccess: suspend (Models.User.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
user: Models.User.DTO.Update,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "${user.id}")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bind(
|
||||
uuid: String,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "binding", "")
|
||||
parameters["user_uuid"] = uuid
|
||||
}.build()
|
||||
val response = httpClient.patch(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun subscribe(
|
||||
id: Int,
|
||||
user: Models.User.DTO.Subscribe,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "$id")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accept(
|
||||
id: Int,
|
||||
user: Models.User.DTO.Accept,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "$id")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reject(
|
||||
id: Int,
|
||||
user: Models.User.DTO.Reject,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "delete", "$id")
|
||||
}.build()
|
||||
val response = httpClient.delete(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(
|
||||
id: Int,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "delete", "$id")
|
||||
}.build()
|
||||
val response = httpClient.delete(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UserRequests {
|
||||
suspend fun get(
|
||||
userId: Int? = null,
|
||||
status: Models.RequestStatus,
|
||||
onSuccess: suspend (List<Models.UserRequest.DTO.Get>) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("requests")
|
||||
userId?.let {
|
||||
parameters.append("user_id", "$it")
|
||||
}
|
||||
parameters.append("status", status.value)
|
||||
}.build()
|
||||
val response = httpClient.get(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
userRequest: Models.UserRequest.DTO.Create,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += "requests"
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(userRequest)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accept(
|
||||
id: Int,
|
||||
request: Models.UserRequest.DTO.Accept,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("requests", "$id")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadImage(
|
||||
type: String,
|
||||
data: ByteArray,
|
||||
onSuccess: suspend (Models.Image.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("requests", "upload_photo")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
setBody(
|
||||
MultiPartFormDataContent(
|
||||
formData {
|
||||
append(
|
||||
key = "file",
|
||||
value = data,
|
||||
headers = Headers.build {
|
||||
append(HttpHeaders.ContentType, type)
|
||||
append(HttpHeaders.ContentDisposition, "form-data; name=\"file\"; filename=\"image\"")
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Municipalities {
|
||||
suspend fun get(
|
||||
onSuccess: suspend (List<Models.Municipality.DTO.Get>) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("districts", "")
|
||||
}.build()
|
||||
val response = httpClient.get(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import io.ktor.http.URLBuilder
|
||||
import io.ktor.http.URLProtocol
|
||||
|
||||
data object Config {
|
||||
const val DEBUG = false
|
||||
private const val TELEGRAM_INIT_DATA = "YOUR-TELEGRAM-INIT-DATA"
|
||||
|
||||
val botUrlBuilder = URLBuilder(
|
||||
protocol = URLProtocol.HTTPS,
|
||||
host = "t.me",
|
||||
pathSegments = listOf("pchelovod71_bot"),
|
||||
)
|
||||
|
||||
val apiUrlBuilder = when (DEBUG) {
|
||||
true -> {
|
||||
URLBuilder(
|
||||
protocol = URLProtocol.HTTP,
|
||||
host = "localhost",
|
||||
port = 5500,
|
||||
pathSegments = listOf("api"),
|
||||
)
|
||||
}
|
||||
false -> {
|
||||
URLBuilder(
|
||||
protocol = URLProtocol.HTTPS,
|
||||
host = "pchelovod71.tularegion.ru",
|
||||
pathSegments = listOf("api"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val telegramInitData = when (DEBUG) {
|
||||
true -> TELEGRAM_INIT_DATA
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object Models {
|
||||
enum class Module {
|
||||
DesktopMain,
|
||||
WasmJsMain,
|
||||
}
|
||||
|
||||
enum class Role {
|
||||
Admin,
|
||||
User,
|
||||
}
|
||||
|
||||
object AuthParams {
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("telegram_id") val telegramId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Send(
|
||||
@SerialName("init_data") val initData: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object User {
|
||||
data class Domain(
|
||||
val id: Int = 0,
|
||||
val role: Role = Role.User,
|
||||
val fullName: String = "",
|
||||
val organization: String = "",
|
||||
val phoneNumber: String = "",
|
||||
val telegramId: String? = null,
|
||||
val municipality: Municipality.Domain,
|
||||
val accepted: Boolean = false,
|
||||
val subscribed: Boolean = false,
|
||||
val deleted: Boolean = false,
|
||||
val deleteReason: String? = null,
|
||||
val uuid: String = "",
|
||||
) {
|
||||
fun toCreateDTO(): DTO.Create {
|
||||
return DTO.Create(
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toGetDTO(),
|
||||
accepted = accepted,
|
||||
)
|
||||
}
|
||||
|
||||
fun toUpdateDTO(): DTO.Update {
|
||||
return DTO.Update(
|
||||
id = id,
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toGetDTO(),
|
||||
accepted = accepted,
|
||||
subscribed = subscribed,
|
||||
)
|
||||
}
|
||||
|
||||
fun toAcceptDTO(): DTO.Accept {
|
||||
return DTO.Accept(
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toGetDTO(),
|
||||
accepted = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun toSubscribeDTO(): DTO.Subscribe {
|
||||
return DTO.Subscribe(
|
||||
subscribed = subscribed,
|
||||
)
|
||||
}
|
||||
|
||||
fun toRejectDTO(): DTO.Reject {
|
||||
return DTO.Reject(
|
||||
deleteReason = deleteReason!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("role_id") val roleId: Int = 2, // TODO: вынужденный костыль, т. к. при создании пользователя в returning возвращается модель отличная от стандартной модели User.DTO.Get
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean,
|
||||
@SerialName("processing_alert_status") val subscribed: Boolean,
|
||||
@SerialName("deleted_status") val deleted: Boolean,
|
||||
@SerialName("deleted_comment") val deleteReason: String?,
|
||||
@SerialName("user_uuid") val uuid: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
role = when (roleId) {
|
||||
1 -> Role.Admin
|
||||
else -> Role.User
|
||||
},
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toDomain(),
|
||||
accepted = accepted,
|
||||
subscribed = subscribed,
|
||||
deleted = deleted,
|
||||
deleteReason = deleteReason,
|
||||
uuid = uuid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Create(
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean = false, // FIXME: зачем принимает verified? Любой идиот может отправить на API null значение и фронт упадет, т. к. полагается на то, что это не nullable значение (пост. Иваницкий Г.О., отв. Бондарев А.И.)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Update(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean,
|
||||
@SerialName("processing_alert_status") val subscribed: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Accept(
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Subscribe(
|
||||
@SerialName("processing_alert_status") val subscribed: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Reject(
|
||||
@SerialName("deleted_comment") val deleteReason: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class RequestStatus(
|
||||
val value: String,
|
||||
) {
|
||||
New("new"),
|
||||
Accepted("accept"),
|
||||
Rejected("reject"),
|
||||
}
|
||||
|
||||
object RequestType {
|
||||
data class Domain(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("request_type_name") val name: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UserRequest {
|
||||
data class Domain(
|
||||
val id: Int = 0,
|
||||
val user: User.Domain,
|
||||
val type: RequestType.Domain = RequestType.Domain(
|
||||
id = 1,
|
||||
name = "Фиксация гибели пчел",
|
||||
),
|
||||
val address: String = "",
|
||||
val text: String = "",
|
||||
val imageList: List<Image.Domain> = emptyList(),
|
||||
val adminComment: String? = null,
|
||||
val accepted: Boolean? = null,
|
||||
val rejectComment: String? = null,
|
||||
) {
|
||||
fun toCreateDTO(): DTO.Create {
|
||||
return DTO.Create(
|
||||
userId = user.id,
|
||||
typeId = type.id,
|
||||
address = address,
|
||||
text = text,
|
||||
imageFileIdList = imageList.map { it.id }
|
||||
)
|
||||
}
|
||||
|
||||
fun toAcceptDTO(): DTO.Accept {
|
||||
return DTO.Accept(
|
||||
adminComment = adminComment,
|
||||
accepted = accepted!!,
|
||||
rejectComment = rejectComment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("user") val user: User.DTO.Get,
|
||||
@SerialName("request_type") val type: RequestType.DTO.Get,
|
||||
@SerialName("address") val address: String,
|
||||
@SerialName("request_description") val text: String,
|
||||
@SerialName("upload_file") val imageList: List<Image.DTO.Get> = emptyList(),
|
||||
@SerialName("admin_correction") val adminComment: String?,
|
||||
@SerialName("status") val accepted: Boolean?,
|
||||
@SerialName("reject_comment") val rejectComment: String?,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
user = user.toDomain(),
|
||||
type = type.toDomain(),
|
||||
address = address,
|
||||
text = text,
|
||||
imageList = imageList.map { it.toDomain() },
|
||||
adminComment = adminComment,
|
||||
accepted = accepted,
|
||||
rejectComment = rejectComment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Create(
|
||||
@SerialName("user_id") val userId: Int,
|
||||
@SerialName("request_type_id") val typeId: Int,
|
||||
@SerialName("address") val address: String,
|
||||
@SerialName("request_description") val text: String,
|
||||
@SerialName("file_id_list") val imageFileIdList: List<Int>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Accept(
|
||||
@SerialName("admin_correction") val adminComment: String?,
|
||||
@SerialName("status") val accepted: Boolean,
|
||||
@SerialName("reject_comment") val rejectComment: String?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Image {
|
||||
data class Domain(
|
||||
val id: Int,
|
||||
val userRequestId: Int?,
|
||||
val fileName: String,
|
||||
val fileSize: Int,
|
||||
val fileType: String,
|
||||
val uploadedAt: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("request_id") val userRequestId: Int?,
|
||||
@SerialName("file_name") val fileName: String,
|
||||
@SerialName("file_size") val fileSize: Int,
|
||||
@SerialName("file_type") val fileType: String,
|
||||
@SerialName("uploaded_at") val uploadedAt: String,
|
||||
@SerialName("url") val url: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
userRequestId = userRequestId,
|
||||
fileName = fileName,
|
||||
fileSize = fileSize,
|
||||
fileType = fileType,
|
||||
uploadedAt = uploadedAt,
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Municipality {
|
||||
data class Domain(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
) {
|
||||
fun toGetDTO(): DTO.Get {
|
||||
return DTO.Get(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("district_name") val name: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.khronos.webgl.get
|
||||
import org.khronos.webgl.set
|
||||
import org.w3c.dom.CanvasRenderingContext2D
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.HTMLTextAreaElement
|
||||
import org.w3c.dom.Image
|
||||
import org.w3c.files.FileReader
|
||||
import org.w3c.files.get
|
||||
import kotlin.math.round
|
||||
|
||||
const val MAX_WIDTH = 1920.0
|
||||
const val MAX_HEIGHT = 1080.0
|
||||
const val TYPE = "image/jpeg"
|
||||
val QUALITY: JsAny = js(".8")
|
||||
val STARTAPP_PATTERN = Regex("^.*tgWebAppStartParam=(.*?)(?:&.*)?$")
|
||||
|
||||
@JsFun("() => getTelegramInitData()")
|
||||
private external fun topLevelGetTelegramInitData(): String
|
||||
|
||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||
object Platform {
|
||||
val module = Models.Module.WasmJsMain
|
||||
|
||||
suspend fun getTelegramInitData() = Config.telegramInitData ?: topLevelGetTelegramInitData()
|
||||
|
||||
suspend fun getBindUuid() = STARTAPP_PATTERN.find(window.location.search)?.groupValues[1]
|
||||
|
||||
fun base64Encode(
|
||||
data: String,
|
||||
) = window.btoa(data)
|
||||
|
||||
suspend fun copyToClipboard(
|
||||
data: String,
|
||||
) {
|
||||
window.navigator.clipboard.writeText(data).await<Nothing?>()
|
||||
}
|
||||
|
||||
fun initIme(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
singleLine: Boolean,
|
||||
label: String,
|
||||
keyboardType: KeyboardType,
|
||||
) {
|
||||
val blank = document.createElement("div") as HTMLDivElement
|
||||
blank.classList.add("blank")
|
||||
blank.addEventListener("click", {
|
||||
blank.remove()
|
||||
})
|
||||
|
||||
val container = document.createElement("div") as HTMLDivElement
|
||||
container.classList.add("container")
|
||||
container.addEventListener("click", { event ->
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
val textField = when (singleLine) {
|
||||
true -> document.createElement("input") as HTMLInputElement
|
||||
false -> document.createElement("textarea") as HTMLTextAreaElement
|
||||
}
|
||||
textField.classList.add("text-field")
|
||||
when (textField) {
|
||||
is HTMLInputElement -> {
|
||||
textField.value = value
|
||||
textField.placeholder = label
|
||||
textField.type = when (keyboardType) {
|
||||
KeyboardType.Text -> "text"
|
||||
KeyboardType.Number -> "number"
|
||||
KeyboardType.Decimal -> "number"
|
||||
KeyboardType.Phone -> "tel"
|
||||
KeyboardType.Email -> "email"
|
||||
KeyboardType.Password -> "password"
|
||||
KeyboardType.NumberPassword -> "password"
|
||||
else -> "text"
|
||||
}
|
||||
}
|
||||
is HTMLTextAreaElement -> {
|
||||
textField.value = value
|
||||
textField.placeholder = label
|
||||
textField.rows = 4
|
||||
}
|
||||
}
|
||||
|
||||
val button = document.createElement("input") as HTMLInputElement
|
||||
button.classList.add("continue")
|
||||
button.type = "button"
|
||||
button.value = "Продолжить"
|
||||
button.addEventListener("click", {
|
||||
blank.remove()
|
||||
})
|
||||
|
||||
container.appendChild(textField)
|
||||
container.appendChild(button)
|
||||
blank.appendChild(container)
|
||||
document.body!!.appendChild(blank)
|
||||
|
||||
textField.focus()
|
||||
|
||||
textField.addEventListener("input", {
|
||||
when (textField) {
|
||||
is HTMLInputElement -> onValueChange(textField.value)
|
||||
is HTMLTextAreaElement -> onValueChange(textField.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pickFile(
|
||||
callback: (String, ByteArray) -> Unit,
|
||||
) {
|
||||
val input = document.createElement("input") as HTMLInputElement
|
||||
input.type = "file"
|
||||
input.style.display = "none"
|
||||
input.accept = "image/*"
|
||||
input.setAttribute("capture", "environment")
|
||||
|
||||
input.onchange = { _ ->
|
||||
input.files?.let { it[0] }?.let { file ->
|
||||
val fileReader = FileReader()
|
||||
|
||||
fileReader.onload = { _ ->
|
||||
val img = Image()
|
||||
|
||||
img.onload = {
|
||||
val canvas = document.createElement("canvas") as HTMLCanvasElement
|
||||
var width = img.width.toDouble()
|
||||
var height = img.height.toDouble()
|
||||
|
||||
if (width > MAX_WIDTH || height > MAX_HEIGHT) {
|
||||
if (width / MAX_WIDTH > height / MAX_HEIGHT) {
|
||||
width = MAX_WIDTH
|
||||
height = round((img.height * (MAX_WIDTH / img.width)))
|
||||
} else {
|
||||
height = MAX_HEIGHT
|
||||
width = round(img.width * (MAX_HEIGHT / img.height))
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width.toInt()
|
||||
canvas.height = height.toInt()
|
||||
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D
|
||||
ctx.drawImage(img, .0, .0, width, height)
|
||||
val dataUrl = canvas.toDataURL(TYPE, QUALITY)
|
||||
|
||||
val base64Data = dataUrl.substringAfter(",")
|
||||
val binaryString = window.atob(base64Data)
|
||||
val uint8Array = Uint8Array(binaryString.length)
|
||||
for (i in binaryString.indices)
|
||||
uint8Array[i] = binaryString[i].code.toByte()
|
||||
val byteArray = ByteArray(uint8Array.length) { i -> uint8Array[i] }
|
||||
callback(
|
||||
file.type,
|
||||
byteArray,
|
||||
)
|
||||
}
|
||||
|
||||
fileReader.result?.let {
|
||||
img.src = "$it"
|
||||
}
|
||||
}
|
||||
|
||||
fileReader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
document.body?.appendChild(input)
|
||||
input.click()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.cit71.pchelovod71
|
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.window.ComposeViewport
|
||||
import kotlinx.browser.document
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun main() {
|
||||
ComposeViewport(document.body!!) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun BooleanFilter(
|
||||
modifier: Modifier = Modifier,
|
||||
valueMap: Map<String, Boolean?>,
|
||||
value: Boolean?,
|
||||
onValueChange: (Boolean?) -> Unit,
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
vertical = 12.dp,
|
||||
),
|
||||
) {
|
||||
valueMap.entries.forEachIndexed { index, entry ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = valueMap.size,
|
||||
),
|
||||
onClick = {
|
||||
onValueChange(entry.value)
|
||||
},
|
||||
selected = value == entry.value,
|
||||
label = {
|
||||
Text(
|
||||
text = entry.key,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ConfirmButton(
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
label: String,
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.then(modifier),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DismissButton(
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
label: String,
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier
|
||||
.then(modifier),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun MainColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
paddingValues: PaddingValues = PaddingValues(),
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(
|
||||
state = rememberScrollState(),
|
||||
)
|
||||
.then(modifier),
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
verticalArrangement = verticalArrangement,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T>MainColumnList(
|
||||
modifier: Modifier = Modifier,
|
||||
paddingValues: PaddingValues = PaddingValues(),
|
||||
onScrollChange: ((Boolean) -> Unit)? = null,
|
||||
valueList: List<T>,
|
||||
placeholder: String,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val verticalScrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
onScrollChange?.let { onScrollChange ->
|
||||
var previousValue = 0
|
||||
snapshotFlow {
|
||||
verticalScrollState.value
|
||||
}.collect {
|
||||
onScrollChange(it <= previousValue)
|
||||
previousValue = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (valueList.isEmpty()) {
|
||||
true -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(
|
||||
state = verticalScrollState,
|
||||
)
|
||||
.then(modifier),
|
||||
) {
|
||||
valueList.forEach {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.round_content_copy_24
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
|
||||
@Composable
|
||||
fun CopyField(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
value: String,
|
||||
label: String,
|
||||
supportingText: String? = null,
|
||||
) {
|
||||
val clipboardCoroutineScope = rememberCoroutineScope()
|
||||
val outlinedTextFieldDefaults = OutlinedTextFieldDefaults.colors()
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.pointerHoverIcon(
|
||||
icon = PointerIcon.Default,
|
||||
overrideDescendants = true,
|
||||
),
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardCoroutineScope.launch {
|
||||
Platform.copyToClipboard(value)
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.round_content_copy_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
supportingText = supportingText?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
colors = outlinedTextFieldDefaults.copy(
|
||||
disabledTextColor = outlinedTextFieldDefaults.unfocusedTextColor,
|
||||
disabledContainerColor = outlinedTextFieldDefaults.unfocusedContainerColor,
|
||||
disabledIndicatorColor = outlinedTextFieldDefaults.unfocusedIndicatorColor,
|
||||
disabledLeadingIconColor = outlinedTextFieldDefaults.unfocusedLeadingIconColor,
|
||||
disabledTrailingIconColor = outlinedTextFieldDefaults.unfocusedTrailingIconColor,
|
||||
disabledLabelColor = outlinedTextFieldDefaults.unfocusedLabelColor,
|
||||
disabledPlaceholderColor = outlinedTextFieldDefaults.unfocusedPlaceholderColor,
|
||||
disabledSupportingTextColor = outlinedTextFieldDefaults.unfocusedSupportingTextColor,
|
||||
disabledPrefixColor = outlinedTextFieldDefaults.unfocusedPrefixColor,
|
||||
disabledSuffixColor = outlinedTextFieldDefaults.unfocusedSuffixColor,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.baseline_close_24
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
interface MainDialogParams {
|
||||
val onDismiss: () -> Unit
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainDialog(
|
||||
title: String,
|
||||
onDismiss: () -> Unit,
|
||||
buttons: @Composable RowScope.() -> Unit,
|
||||
content: @Composable (MainScaffoldParams) -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
),
|
||||
) {
|
||||
MainScaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.baseline_close_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = buttons,
|
||||
)
|
||||
}
|
||||
) { mainScaffoldParams ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
) {
|
||||
content(mainScaffoldParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun <T>DropdownField(
|
||||
modifier: Modifier = Modifier,
|
||||
valueList: List<T>,
|
||||
value: T,
|
||||
onValueChange: (T) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
label: String,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = enabled && expanded,
|
||||
onExpandedChange = {
|
||||
expanded = it
|
||||
}
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(
|
||||
type = ExposedDropdownMenuAnchorType.PrimaryEditable,
|
||||
)
|
||||
.pointerHoverIcon(
|
||||
icon = PointerIcon.Default,
|
||||
overrideDescendants = true,
|
||||
),
|
||||
enabled = enabled,
|
||||
readOnly = true,
|
||||
value = value.toString(),
|
||||
onValueChange = {},
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = enabled && expanded
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = enabled && expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
valueList.forEach {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = it.toString(),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onValueChange(it)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.carousel.CarouselState
|
||||
import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.baseline_close_24
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
enum class InteractionType {
|
||||
Upload,
|
||||
Download,
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ImagePicker(
|
||||
imageList: List<Models.Image.Domain>,
|
||||
onImageAdd: (() -> Unit)? = null,
|
||||
onImageDrop: (Int) -> Unit,
|
||||
onImageSelect: (Models.Image.Domain) -> Unit,
|
||||
interactionType: InteractionType,
|
||||
) {
|
||||
Column {
|
||||
if (interactionType == InteractionType.Upload)
|
||||
onImageAdd?.let {
|
||||
OutlinedButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onClick = it,
|
||||
) {
|
||||
Text(
|
||||
text = "Добавить изображение",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (imageList.size) {
|
||||
0 -> {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = 4.dp,
|
||||
),
|
||||
text = "Изображения не выбраны",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
HorizontalMultiBrowseCarousel(
|
||||
state = remember(imageList.size) {
|
||||
CarouselState(
|
||||
currentItem = imageList.size,
|
||||
itemCount = {
|
||||
imageList.size
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(
|
||||
vertical = 8.dp,
|
||||
),
|
||||
preferredItemWidth = 186.dp,
|
||||
itemSpacing = 8.dp,
|
||||
) { i ->
|
||||
val image = imageList[i]
|
||||
Box(
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.height(205.dp)
|
||||
.maskClip(MaterialTheme.shapes.extraLarge)
|
||||
.clickable {
|
||||
onImageSelect(image)
|
||||
},
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = image.url,
|
||||
contentScale = ContentScale.None,
|
||||
),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
|
||||
if (interactionType == InteractionType.Upload)
|
||||
IconButton(
|
||||
onClick = {
|
||||
onImageDrop(i)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.baseline_close_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (interactionType == InteractionType.Upload && imageList.size > 1)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Для прокрутки изображений на компьютерах и ноутбуках удерживайте клавишу Shift",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.material3.LoadingIndicatorDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun MainLoadingIndicator() {
|
||||
LoadingIndicator(
|
||||
modifier = Modifier
|
||||
.size(
|
||||
width = LoadingIndicatorDefaults.ContainerWidth * 3,
|
||||
height = LoadingIndicatorDefaults.ContainerHeight * 3,
|
||||
),
|
||||
)
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.round_check_24
|
||||
import frontend.composeapp.generated.resources.round_close_24
|
||||
import frontend.composeapp.generated.resources.round_manage_accounts_24
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
@Composable
|
||||
fun RegistrationRequestCard(
|
||||
userDomain: Models.User.Domain,
|
||||
selected: Boolean,
|
||||
onSelectedChange: ((Models.User.Domain)?) -> Unit,
|
||||
onAccept: (Models.User.Domain) -> Unit,
|
||||
onEdit: (Models.User.Domain) -> Unit,
|
||||
onDecline: (Models.User.Domain) -> Unit,
|
||||
) {
|
||||
val backgroundColor = animateColorAsState(
|
||||
targetValue = when (selected) {
|
||||
true -> MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.12f,
|
||||
)
|
||||
false -> Color.Transparent
|
||||
},
|
||||
)
|
||||
val horizontalPadding = animateDpAsState(
|
||||
targetValue = when (selected) {
|
||||
true -> 16.dp
|
||||
false -> 0.dp
|
||||
},
|
||||
)
|
||||
val shapeState = animateDpAsState(
|
||||
targetValue = when (selected) {
|
||||
true -> 28.0.dp
|
||||
false -> 0.0.dp
|
||||
},
|
||||
)
|
||||
val cardColors = CardDefaults.cardColors()
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontalPadding.value),
|
||||
shape = RoundedCornerShape(shapeState.value),
|
||||
colors = CardColors(
|
||||
containerColor = backgroundColor.value,
|
||||
contentColor = cardColors.contentColor,
|
||||
disabledContainerColor = cardColors.disabledContainerColor,
|
||||
disabledContentColor = cardColors.disabledContentColor,
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onSelectedChange(
|
||||
when (selected) {
|
||||
true -> null
|
||||
false -> userDomain
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = userDomain.fullName,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = userDomain.municipality.name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 3,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = selected,
|
||||
) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
RegistrationRequestButton(
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1f,
|
||||
fill = true,
|
||||
),
|
||||
title = "Принять",
|
||||
painter = painterResource(Res.drawable.round_check_24),
|
||||
onClick = {
|
||||
onAccept(userDomain)
|
||||
},
|
||||
)
|
||||
RegistrationRequestButton(
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1f,
|
||||
fill = true,
|
||||
),
|
||||
title = "Изменить",
|
||||
painter = painterResource(Res.drawable.round_manage_accounts_24),
|
||||
onClick = {
|
||||
onEdit(userDomain)
|
||||
},
|
||||
)
|
||||
RegistrationRequestButton(
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1f,
|
||||
fill = true,
|
||||
),
|
||||
title = "Отклонить",
|
||||
painter = painterResource(Res.drawable.round_close_24),
|
||||
onClick = {
|
||||
onDecline(userDomain)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RegistrationRequestButton(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
painter: Painter,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxHeight(),
|
||||
shape = RectangleShape,
|
||||
colors = ButtonDefaults.textButtonColors(),
|
||||
contentPadding = PaddingValues(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.fillMaxHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class MainScaffoldParams(
|
||||
val paddingValues: PaddingValues,
|
||||
val showSuccessSnackbar: (String) -> Unit,
|
||||
val showErrorSnackbar: (String) -> Unit,
|
||||
val setFloatingActionButton: ((@Composable AnimatedVisibilityScope.() -> Unit)?) -> Unit,
|
||||
val withLoadingIndicator: suspend (suspend () -> Unit) -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MainScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
topBar: @Composable () -> Unit = {},
|
||||
bottomBar: @Composable () -> Unit = {},
|
||||
content: (@Composable (MainScaffoldParams) -> Unit)? = null,
|
||||
) {
|
||||
var loadingIndicator by remember { mutableStateOf(false) }
|
||||
|
||||
suspend fun withLoadingIndicator(
|
||||
function: suspend () -> Unit,
|
||||
) {
|
||||
loadingIndicator = true
|
||||
try {
|
||||
function()
|
||||
} finally {
|
||||
loadingIndicator = false
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarCoroutineScope = rememberCoroutineScope()
|
||||
val successSnackbarHostState = SnackbarHostState()
|
||||
val errorSnackbarHostState = SnackbarHostState()
|
||||
|
||||
fun showSuccessSnackbar(
|
||||
message: String,
|
||||
) {
|
||||
snackbarCoroutineScope.launch {
|
||||
successSnackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
withDismissAction = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showErrorSnackbar(
|
||||
message: String,
|
||||
) {
|
||||
snackbarCoroutineScope.launch {
|
||||
errorSnackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
withDismissAction = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var floatingActionButton by remember { mutableStateOf<(@Composable AnimatedVisibilityScope.() -> Unit)?>(null) }
|
||||
var floatingActionButtonVisible by remember { mutableStateOf(false) }
|
||||
|
||||
fun setFloatingActionButton(
|
||||
content: (@Composable AnimatedVisibilityScope.() -> Unit)?,
|
||||
) {
|
||||
when (content) {
|
||||
null -> {
|
||||
floatingActionButtonVisible = false
|
||||
}
|
||||
else -> {
|
||||
floatingActionButton = content
|
||||
floatingActionButtonVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier),
|
||||
topBar = topBar,
|
||||
bottomBar = bottomBar,
|
||||
floatingActionButton = {
|
||||
floatingActionButton?.let {
|
||||
AnimatedVisibility(
|
||||
visible = floatingActionButtonVisible,
|
||||
enter = fadeIn() + expandHorizontally(),
|
||||
exit = shrinkHorizontally() + fadeOut(),
|
||||
content = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
MainSnackbarHost(
|
||||
successSnackbarHostState = successSnackbarHostState,
|
||||
errorSnackbarHostState = errorSnackbarHostState,
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
content?.let {
|
||||
it(
|
||||
MainScaffoldParams(
|
||||
paddingValues = paddingValues,
|
||||
showSuccessSnackbar = ::showSuccessSnackbar,
|
||||
showErrorSnackbar = ::showErrorSnackbar,
|
||||
setFloatingActionButton = ::setFloatingActionButton,
|
||||
withLoadingIndicator = ::withLoadingIndicator,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
visible = loadingIndicator,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
color = when (content) {
|
||||
null -> Color.Unspecified
|
||||
else -> Color.Black.copy(
|
||||
alpha = .5f,
|
||||
)
|
||||
},
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
awaitPointerEvent()
|
||||
}
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
MainLoadingIndicator()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SettingCard(
|
||||
name: String,
|
||||
caption: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: ((Boolean) -> Unit),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
){
|
||||
Text(
|
||||
text = name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = caption,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
fun MainSnackbarHost(
|
||||
successSnackbarHostState: SnackbarHostState,
|
||||
errorSnackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val successContainerDark = Color(0xFF009424)
|
||||
val onSuccessContainerDark = Color(0xFFD6FFEA)
|
||||
|
||||
SnackbarHost(successSnackbarHostState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
contentColor = onSuccessContainerDark,
|
||||
containerColor = successContainerDark,
|
||||
dismissActionContentColor = onSuccessContainerDark,
|
||||
)
|
||||
}
|
||||
|
||||
SnackbarHost(errorSnackbarHostState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
dismissActionContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ru.cit71.pchelovod71.ui.components// TODO: спойлер для расширенных параметров пользователя; не пригодился, но оставлен до востребования
|
||||
|
||||
//package ru.cit71.bee_frontend.ui.components
|
||||
//
|
||||
//import androidx.compose.animation.AnimatedVisibility
|
||||
//import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
//import androidx.compose.animation.core.animateFloatAsState
|
||||
//import androidx.compose.animation.core.tween
|
||||
//import androidx.compose.foundation.layout.Arrangement
|
||||
//import androidx.compose.foundation.layout.Column
|
||||
//import androidx.compose.foundation.layout.ColumnScope
|
||||
//import androidx.compose.foundation.layout.Row
|
||||
//import androidx.compose.foundation.layout.fillMaxWidth
|
||||
//import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
//import androidx.compose.material3.Icon
|
||||
//import androidx.compose.material3.IconToggleButton
|
||||
//import androidx.compose.material3.MaterialTheme
|
||||
//import androidx.compose.material3.Text
|
||||
//import androidx.compose.runtime.Composable
|
||||
//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.Modifier
|
||||
//import androidx.compose.ui.draw.rotate
|
||||
//import androidx.compose.ui.text.style.TextOverflow
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import frontend.composeapp.generated.resources.Res
|
||||
//import frontend.composeapp.generated.resources.round_arrow_right_24
|
||||
//import org.jetbrains.compose.resources.painterResource
|
||||
//
|
||||
//@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
//@Composable
|
||||
//fun Spoiler(
|
||||
// name: String,
|
||||
// content: @Composable ColumnScope.() -> Unit,
|
||||
//) {
|
||||
// var expanded by remember { mutableStateOf(false) }
|
||||
// val rotation by animateFloatAsState(
|
||||
// targetValue = if (expanded) 90f else 0f,
|
||||
// animationSpec = tween(
|
||||
// durationMillis = 300,
|
||||
// easing = FastOutSlowInEasing
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// Column(
|
||||
// verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
// ) {
|
||||
// Row(
|
||||
// horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// ) {
|
||||
// IconToggleButton(
|
||||
// checked = expanded,
|
||||
// onCheckedChange = {
|
||||
// expanded = it
|
||||
// },
|
||||
// ) {
|
||||
// Icon(
|
||||
// modifier = Modifier
|
||||
// .rotate(rotation),
|
||||
// painter = painterResource(Res.drawable.round_arrow_right_24),
|
||||
// contentDescription = null,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// Text(
|
||||
// text = name,
|
||||
// overflow = TextOverflow.Ellipsis,
|
||||
// softWrap = false,
|
||||
// style = MaterialTheme.typography.titleMedium,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// AnimatedVisibility(
|
||||
// visible = expanded,
|
||||
// ) {
|
||||
// Column(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth(),
|
||||
// verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
// content = content,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,89 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
|
||||
@Composable
|
||||
fun MainTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
label: String,
|
||||
singleLine: Boolean = true,
|
||||
minLines: Int = 1,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
keyboardType: KeyboardType = KeyboardType.Unspecified,
|
||||
) {
|
||||
val outlinedTextFieldDefaults = OutlinedTextFieldDefaults.colors()
|
||||
|
||||
val moduleModifier = when (Platform.module) {
|
||||
Models.Module.DesktopMain -> Modifier
|
||||
Models.Module.WasmJsMain -> Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
) {
|
||||
if (enabled)
|
||||
Platform.initIme(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
label = label,
|
||||
keyboardType = keyboardType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(moduleModifier),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
enabled = when (Platform.module) {
|
||||
Models.Module.DesktopMain -> enabled
|
||||
Models.Module.WasmJsMain -> false
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType,
|
||||
),
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
colors = when (Platform.module) {
|
||||
Models.Module.DesktopMain -> outlinedTextFieldDefaults
|
||||
Models.Module.WasmJsMain -> when (enabled) {
|
||||
true -> outlinedTextFieldDefaults.copy(
|
||||
disabledTextColor = outlinedTextFieldDefaults.unfocusedTextColor,
|
||||
disabledContainerColor = outlinedTextFieldDefaults.unfocusedContainerColor,
|
||||
disabledIndicatorColor = outlinedTextFieldDefaults.unfocusedIndicatorColor,
|
||||
disabledLeadingIconColor = outlinedTextFieldDefaults.unfocusedLeadingIconColor,
|
||||
disabledTrailingIconColor = outlinedTextFieldDefaults.unfocusedTrailingIconColor,
|
||||
disabledLabelColor = outlinedTextFieldDefaults.unfocusedLabelColor,
|
||||
disabledPlaceholderColor = outlinedTextFieldDefaults.unfocusedPlaceholderColor,
|
||||
disabledSupportingTextColor = outlinedTextFieldDefaults.unfocusedSupportingTextColor,
|
||||
disabledPrefixColor = outlinedTextFieldDefaults.unfocusedPrefixColor,
|
||||
disabledSuffixColor = outlinedTextFieldDefaults.unfocusedSuffixColor,
|
||||
)
|
||||
false -> outlinedTextFieldDefaults
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
@Composable
|
||||
fun UserCard(
|
||||
userDomain: Models.User.Domain,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
){
|
||||
Text(
|
||||
text = userDomain.fullName,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = userDomain.organization,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = userDomain.phoneNumber,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = userDomain.municipality.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
@Composable
|
||||
fun UserRequestCard(
|
||||
userRequestDomain: Models.UserRequest.Domain,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
){
|
||||
Text(
|
||||
text = userRequestDomain.type.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = userRequestDomain.user.municipality.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = userRequestDomain.address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = when (userRequestDomain.accepted) {
|
||||
true -> "Принято"
|
||||
false -> "Отклонено"
|
||||
null -> "Новое"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import ru.cit71.pchelovod71.ui.components.ConfirmButton
|
||||
import ru.cit71.pchelovod71.ui.components.DismissButton
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialogParams
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
sealed interface ConfirmDialogParams : MainDialogParams
|
||||
|
||||
data class ConfirmCommentDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onConfirm: (String) -> Unit,
|
||||
) : ConfirmDialogParams
|
||||
|
||||
data class ConfirmOnlyDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onConfirm: () -> Unit,
|
||||
val title: String,
|
||||
) : ConfirmDialogParams
|
||||
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
confirmDialogParams: ConfirmDialogParams,
|
||||
dismissButtonText: String,
|
||||
confirmButtonText: String,
|
||||
) {
|
||||
var comment by remember { mutableStateOf("") }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = confirmDialogParams.onDismiss,
|
||||
properties = DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.wrapContentSize(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
when (confirmDialogParams) {
|
||||
is ConfirmCommentDialogParams -> {
|
||||
MainTextField(
|
||||
value = comment,
|
||||
onValueChange = {
|
||||
comment = it
|
||||
},
|
||||
label = "Комментарий",
|
||||
keyboardType = KeyboardType.Text,
|
||||
singleLine = false,
|
||||
minLines = 3,
|
||||
maxLines = 6,
|
||||
)
|
||||
}
|
||||
is ConfirmOnlyDialogParams -> {
|
||||
Text(
|
||||
text = confirmDialogParams.title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
when (confirmDialogParams) {
|
||||
is ConfirmCommentDialogParams -> {
|
||||
confirmDialogParams.onConfirm(comment)
|
||||
}
|
||||
is ConfirmOnlyDialogParams -> {
|
||||
confirmDialogParams.onConfirm()
|
||||
}
|
||||
}
|
||||
},
|
||||
label = confirmButtonText,
|
||||
)
|
||||
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
confirmDialogParams.onDismiss()
|
||||
},
|
||||
label = dismissButtonText,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.gestures.TransformableState
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun ImageViewDialog(
|
||||
image: Models.Image.Domain,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
var zoom by remember { mutableStateOf(1f) }
|
||||
var pan by remember { mutableStateOf(Offset.Zero) }
|
||||
var rotation by remember { mutableStateOf(0f) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.transformable(
|
||||
state = TransformableState { zoomChange, panChange, rotationChange ->
|
||||
zoom = max(zoom * zoomChange, 1f)
|
||||
pan += panChange
|
||||
rotation += rotationChange
|
||||
},
|
||||
),
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = image.url,
|
||||
contentScale = ContentScale.None,
|
||||
),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.ktor.http.clone
|
||||
import ru.cit71.pchelovod71.core.Config
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.ConfirmButton
|
||||
import ru.cit71.pchelovod71.ui.components.CopyField
|
||||
import ru.cit71.pchelovod71.ui.components.DismissButton
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialog
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialogParams
|
||||
import ru.cit71.pchelovod71.ui.components.DropdownField
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
sealed interface UserDialogParams : MainDialogParams
|
||||
|
||||
data class CreateUserDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onCreate: ((Models.User.Domain) -> Unit),
|
||||
) : UserDialogParams
|
||||
|
||||
data class UpdateUserDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onSave: ((Models.User.Domain) -> Unit),
|
||||
val onDelete: ((Models.User.Domain) -> Unit),
|
||||
) : UserDialogParams
|
||||
|
||||
data class AcceptUserDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onAccept: ((Models.User.Domain) -> Unit),
|
||||
val onDecline: ((Models.User.Domain) -> Unit),
|
||||
) : UserDialogParams
|
||||
|
||||
@Composable
|
||||
fun UserDialog(
|
||||
userDomain: Models.User.Domain,
|
||||
municipalityDomainList: List<Models.Municipality.Domain>,
|
||||
userDialogParams: UserDialogParams,
|
||||
) {
|
||||
var outUserDomain by remember { mutableStateOf(userDomain) }
|
||||
|
||||
MainDialog(
|
||||
title = when (userDialogParams) {
|
||||
is CreateUserDialogParams -> "Добавить пользователя"
|
||||
is UpdateUserDialogParams -> "Изменить пользователя"
|
||||
is AcceptUserDialogParams -> "Принять заявку на регистрацию"
|
||||
},
|
||||
onDismiss = userDialogParams.onDismiss,
|
||||
buttons = {
|
||||
when (userDialogParams) {
|
||||
is CreateUserDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onCreate(outUserDomain)
|
||||
},
|
||||
label = "Добавить",
|
||||
)
|
||||
}
|
||||
is UpdateUserDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onSave(outUserDomain)
|
||||
},
|
||||
label = "Сохранить",
|
||||
)
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onDelete(outUserDomain)
|
||||
},
|
||||
label = "Удалить",
|
||||
)
|
||||
}
|
||||
is AcceptUserDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onAccept(outUserDomain)
|
||||
},
|
||||
label = "Принять",
|
||||
)
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onDecline(outUserDomain)
|
||||
},
|
||||
label = "Отклонить",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { mainScaffoldParams ->
|
||||
MainColumn(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MainTextField(
|
||||
value = outUserDomain.fullName,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
fullName = it,
|
||||
)
|
||||
},
|
||||
label = "ФИО",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserDomain.organization,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
organization = it,
|
||||
)
|
||||
},
|
||||
label = "Организация",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserDomain.phoneNumber,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
phoneNumber = it,
|
||||
)
|
||||
},
|
||||
label = "Номер телефона",
|
||||
keyboardType = KeyboardType.Phone,
|
||||
)
|
||||
|
||||
DropdownField(
|
||||
valueList = municipalityDomainList,
|
||||
value = outUserDomain.municipality,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
municipality = it,
|
||||
)
|
||||
},
|
||||
label = "Муниципальное образование",
|
||||
)
|
||||
|
||||
if (userDialogParams is UpdateUserDialogParams && userDomain.telegramId == null) {
|
||||
val inviteUrl = Config.botUrlBuilder.clone().apply {
|
||||
parameters["startapp"] = userDomain.uuid
|
||||
}.buildString()
|
||||
|
||||
CopyField(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
value = inviteUrl,
|
||||
label = "Пригласительная ссылка",
|
||||
supportingText = "Пользователь не привязан к учетной записи Telegram. Скопируйте ссылку и передайте ее владельцу",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.ConfirmButton
|
||||
import ru.cit71.pchelovod71.ui.components.DismissButton
|
||||
import ru.cit71.pchelovod71.ui.components.DropdownField
|
||||
import ru.cit71.pchelovod71.ui.components.ImagePicker
|
||||
import ru.cit71.pchelovod71.ui.components.InteractionType
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialog
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialogParams
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
sealed interface UserRequestDialogParams : MainDialogParams
|
||||
|
||||
data class ShowUserRequestDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
) : UserRequestDialogParams
|
||||
|
||||
data class CreateUserRequestDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onCreate: (Models.UserRequest.Domain) -> Unit,
|
||||
) : UserRequestDialogParams
|
||||
|
||||
data class AcceptUserRequestDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onAccept: ((Models.UserRequest.Domain) -> Unit),
|
||||
val onDecline: ((Models.UserRequest.Domain) -> Unit),
|
||||
) : UserRequestDialogParams
|
||||
|
||||
@Composable
|
||||
fun UserRequestDialog(
|
||||
userRequestDomain: Models.UserRequest.Domain,
|
||||
userRequestDialogParams: UserRequestDialogParams,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var outUserRequestDomain by remember { mutableStateOf(userRequestDomain) }
|
||||
|
||||
var imageView by remember { mutableStateOf<Models.Image.Domain?>(null) }
|
||||
|
||||
MainDialog(
|
||||
title = when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> "Просмотреть обращение"
|
||||
is CreateUserRequestDialogParams -> "Добавить обращение"
|
||||
is AcceptUserRequestDialogParams -> "Принять обращение"
|
||||
},
|
||||
onDismiss = userRequestDialogParams.onDismiss,
|
||||
buttons = {
|
||||
when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> {}
|
||||
is CreateUserRequestDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userRequestDialogParams.onCreate(outUserRequestDomain)
|
||||
},
|
||||
label = "Добавить",
|
||||
)
|
||||
}
|
||||
is AcceptUserRequestDialogParams -> {
|
||||
if (userRequestDomain.accepted == null) {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userRequestDialogParams.onAccept(outUserRequestDomain)
|
||||
},
|
||||
label = "Принять",
|
||||
)
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userRequestDialogParams.onDecline(outUserRequestDomain)
|
||||
},
|
||||
label = "Отклонить",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { mainScaffoldParams ->
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
bottom = 16.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MainTextField(
|
||||
value = userRequestDomain.user.fullName,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "ФИО",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userRequestDomain.user.organization,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "Организация",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userRequestDomain.user.phoneNumber,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "Номер телефона",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.user.municipality.name,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "Муниципальное образование",
|
||||
)
|
||||
|
||||
val enabledField = when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> false
|
||||
is AcceptUserRequestDialogParams -> false
|
||||
is CreateUserRequestDialogParams -> true
|
||||
}
|
||||
|
||||
val requestTypeList = listOf( // TODO: по хорошему, нужно стягивать список по API
|
||||
Models.RequestType.Domain(
|
||||
id = 1,
|
||||
name = "Фиксация гибели пчел",
|
||||
),
|
||||
Models.RequestType.Domain(
|
||||
id = 2,
|
||||
name = "Фиксация факта нарушения сроков обработки полей",
|
||||
),
|
||||
)
|
||||
|
||||
DropdownField(
|
||||
valueList = requestTypeList,
|
||||
value = outUserRequestDomain.type,
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
type = it,
|
||||
)
|
||||
},
|
||||
enabled = enabledField,
|
||||
label = "Тип обращения",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.address,
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
address = it,
|
||||
)
|
||||
},
|
||||
enabled = enabledField,
|
||||
label = "Адрес",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.text,
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
text = it,
|
||||
)
|
||||
},
|
||||
enabled = enabledField,
|
||||
label = "Сутьевая часть",
|
||||
keyboardType = KeyboardType.Text,
|
||||
singleLine = false,
|
||||
minLines = 3,
|
||||
maxLines = 6,
|
||||
)
|
||||
|
||||
if (userRequestDialogParams is AcceptUserRequestDialogParams)
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.adminComment ?: "",
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
adminComment = it,
|
||||
)
|
||||
},
|
||||
enabled = userRequestDomain.accepted == null,
|
||||
label = "Уточнение администратора",
|
||||
keyboardType = KeyboardType.Text,
|
||||
singleLine = false,
|
||||
minLines = 3,
|
||||
maxLines = 6,
|
||||
)
|
||||
|
||||
ImagePicker(
|
||||
imageList = outUserRequestDomain.imageList,
|
||||
onImageAdd = {
|
||||
Platform.pickFile { type, data ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.uploadImage(
|
||||
type = type,
|
||||
data = data,
|
||||
onSuccess = { imageGetDTO ->
|
||||
val imageList = outUserRequestDomain.imageList.toMutableList()
|
||||
imageList.add(imageGetDTO.toDomain())
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
imageList = imageList,
|
||||
)
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Файл успешно добавлен",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось добавить файл",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onImageDrop = { i ->
|
||||
val imageList = outUserRequestDomain.imageList.toMutableList()
|
||||
imageList.removeAt(i)
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
imageList = imageList,
|
||||
)
|
||||
},
|
||||
onImageSelect = {
|
||||
imageView = it
|
||||
},
|
||||
interactionType = when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> InteractionType.Download
|
||||
is CreateUserRequestDialogParams -> InteractionType.Upload
|
||||
is AcceptUserRequestDialogParams -> InteractionType.Download
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
imageView?.let {
|
||||
ImageViewDialog(
|
||||
image = it,
|
||||
onClose = {
|
||||
imageView = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumnList
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.RegistrationRequestCard
|
||||
import ru.cit71.pchelovod71.ui.dialogs.AcceptUserDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmCommentDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserDialog
|
||||
|
||||
@Composable
|
||||
fun RegistrationRequestsScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userDomainList by remember { mutableStateOf(listOf<Models.User.Domain>()) }
|
||||
var municipalityDomainList by remember { mutableStateOf(listOf<Models.Municipality.Domain>()) }
|
||||
|
||||
var userDomainCard by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
var userDomainDialog by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
var confirmCommentDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Municipalities.get(
|
||||
onSuccess = { municipalityDTOList ->
|
||||
municipalityDomainList = municipalityDTOList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список муниципальных образований"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserDomainList() {
|
||||
Api.Users.get(
|
||||
verified = false,
|
||||
onSuccess = { userDTOList ->
|
||||
userDomainList = userDTOList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список запросов на регистрацию",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onAccept(
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
Api.Users.accept(
|
||||
id = userDomain.id,
|
||||
user = userDomain.toAcceptDTO(),
|
||||
onSuccess = {
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно принят",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось принять пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
|
||||
MainColumnList(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
valueList = userDomainList,
|
||||
placeholder = "Список заявок на регистрацию пуст",
|
||||
) {
|
||||
RegistrationRequestCard(
|
||||
userDomain = it,
|
||||
selected = it == userDomainCard,
|
||||
onSelectedChange = {
|
||||
userDomainCard = it
|
||||
},
|
||||
onAccept = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
onAccept(outUserDomain)
|
||||
}
|
||||
}
|
||||
},
|
||||
onEdit = {
|
||||
userDomainDialog = it
|
||||
},
|
||||
onDecline = { outUserDomain ->
|
||||
userDomainCard = outUserDomain
|
||||
confirmCommentDialog = true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
userDomainDialog?.let { inUserDomain ->
|
||||
UserDialog(
|
||||
userDomain = inUserDomain,
|
||||
municipalityDomainList = municipalityDomainList,
|
||||
userDialogParams = AcceptUserDialogParams(
|
||||
onDismiss = {
|
||||
userDomainDialog = null
|
||||
},
|
||||
onAccept = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
onAccept(outUserDomain)
|
||||
}
|
||||
}
|
||||
userDomainDialog = null
|
||||
},
|
||||
onDecline = { outUserDomain ->
|
||||
userDomainCard = outUserDomain
|
||||
confirmCommentDialog = true
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
userDomainCard?.let { inUserDomain ->
|
||||
if (confirmCommentDialog)
|
||||
ConfirmDialog(
|
||||
confirmDialogParams = ConfirmCommentDialogParams(
|
||||
onDismiss = {
|
||||
confirmCommentDialog = false
|
||||
},
|
||||
onConfirm = { comment ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.reject(
|
||||
id = inUserDomain.id,
|
||||
user = inUserDomain.copy(
|
||||
deleteReason = comment,
|
||||
).toRejectDTO(),
|
||||
onSuccess = {
|
||||
confirmCommentDialog = false
|
||||
userDomainDialog = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно отклонен",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось отклонить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
dismissButtonText = "Отмена",
|
||||
confirmButtonText = "Отправить",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.SettingCard
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.auth(
|
||||
authParams = Models.AuthParams.DTO.Send(
|
||||
initData = Platform.getTelegramInitData(),
|
||||
),
|
||||
onSuccess = {
|
||||
userDomain = it.toDomain()
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить текущие настройки пользователя"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MainColumn {
|
||||
userDomain?.let {
|
||||
SettingCard(
|
||||
name = "Уведомления",
|
||||
caption = "Подписаться на уведомления по инцидентам вашего муниципального образования",
|
||||
checked = userDomain!!.subscribed,
|
||||
onCheckedChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
subscribed = it
|
||||
)
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.subscribe(
|
||||
id = userDomain!!.id,
|
||||
user = userDomain!!.toSubscribeDTO(),
|
||||
onSuccess = {
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Настройки пользователя успешно применены"
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось применить настройки пользователя"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.DropdownField
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
@Composable
|
||||
fun RegistrationUserScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
telegramId: String,
|
||||
reloadUserDomain: suspend () -> Unit,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var municipalityDomainList by remember { mutableStateOf(listOf<Models.Municipality.Domain>()) }
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Municipalities.get(
|
||||
onSuccess = { municipalityDTOList ->
|
||||
municipalityDomainList = municipalityDTOList.map { it.toDomain() }
|
||||
|
||||
userDomain = Models.User.Domain(
|
||||
telegramId = telegramId,
|
||||
municipality = municipalityDomainList[5],
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список муниципальных образований"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userDomain?.let {
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Помощник пчеловода",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Заявка на регистрацию",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MainTextField(
|
||||
value = userDomain!!.fullName,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
fullName = it,
|
||||
)
|
||||
},
|
||||
label = "ФИО",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
DropdownField(
|
||||
valueList = municipalityDomainList,
|
||||
value = userDomain!!.municipality,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
municipality = it,
|
||||
)
|
||||
},
|
||||
label = "Муниципальное образование",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userDomain!!.organization,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
organization = it,
|
||||
)
|
||||
},
|
||||
label = "Организация",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userDomain!!.phoneNumber,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
phoneNumber = it,
|
||||
)
|
||||
},
|
||||
label = "Номер телефона",
|
||||
keyboardType = KeyboardType.Phone,
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.create(
|
||||
user = userDomain!!.toCreateDTO(),
|
||||
onSuccess = {
|
||||
reloadUserDomain()
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Заявка на регистрацию успешно отправлена!"
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось отправить заявку на регистрацию"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = "Отправить",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WaitRegistrationUserScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Помощник пчеловода",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Заявка на регистрацию находится на рассмотрении сотрудников министерства сельского хозяйства, природных ресурсов и экологии Тульской области. После одобрения вам поступит сообщение от чат-бота.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RejectRegistrationUserScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Помощник пчеловода",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Заявка на регистрацию была отклонена сотрудником министерства сельского хозяйства, природных ресурсов и экологии Тульской области.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.outline_add_24
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.BooleanFilter
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumnList
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.UserRequestCard
|
||||
import ru.cit71.pchelovod71.ui.dialogs.AcceptUserRequestDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmCommentDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.CreateUserRequestDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ShowUserRequestDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserRequestDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserRequestDialogParams
|
||||
|
||||
@Composable
|
||||
fun UserRequestsScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userRequestDomainList by remember { mutableStateOf(listOf<Models.UserRequest.Domain>()) }
|
||||
|
||||
var userRequestDomain by remember { mutableStateOf<Models.UserRequest.Domain?>(null) }
|
||||
var userRequestDialogParams by remember { mutableStateOf<UserRequestDialogParams?>(null) }
|
||||
|
||||
var confirmCommentDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var floatingActionButton by remember { mutableStateOf(
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> false
|
||||
Models.Role.User -> true
|
||||
}
|
||||
) }
|
||||
|
||||
var acceptedFilter by remember { mutableStateOf<Boolean?>(null) }
|
||||
|
||||
suspend fun updateUserRequestDomainList() {
|
||||
Api.UserRequests.get(
|
||||
userId = when (userDomain.role) {
|
||||
Models.Role.Admin -> null
|
||||
Models.Role.User -> userDomain.id
|
||||
},
|
||||
status = when (acceptedFilter) {
|
||||
null -> Models.RequestStatus.New
|
||||
true -> Models.RequestStatus.Accepted
|
||||
false -> Models.RequestStatus.Rejected
|
||||
},
|
||||
onSuccess = { userRequestList ->
|
||||
userRequestDomainList = userRequestList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список обращений",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
userRequestDomain = null
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> {
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
}
|
||||
Models.Role.User -> {
|
||||
mainScaffoldParams.setFloatingActionButton {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
text = "Добавить обращение",
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.outline_add_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
userRequestDialogParams = CreateUserRequestDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onCreate = { outUserRequestDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.create(
|
||||
userRequest = outUserRequestDomain.toCreateDTO(),
|
||||
onSuccess = {
|
||||
userRequestDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Обращение успешно добавлено",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось добавить обращение",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
userRequestDomain = Models.UserRequest.Domain(
|
||||
user = userDomain,
|
||||
)
|
||||
},
|
||||
expanded = floatingActionButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(mainScaffoldParams.paddingValues),
|
||||
) {
|
||||
BooleanFilter(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
valueMap = mapOf(
|
||||
"Принято" to true,
|
||||
"Новые" to null,
|
||||
"Отклонено" to false,
|
||||
),
|
||||
value = acceptedFilter,
|
||||
onValueChange = {
|
||||
acceptedFilter = it
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MainColumnList(
|
||||
onScrollChange = {
|
||||
floatingActionButton = it
|
||||
},
|
||||
valueList = userRequestDomainList,
|
||||
placeholder = "Список обращений пуст",
|
||||
) {
|
||||
UserRequestCard(
|
||||
userRequestDomain = it,
|
||||
onClick = {
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> {
|
||||
userRequestDialogParams = AcceptUserRequestDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onAccept = { outUserRequestDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.accept(
|
||||
id = it.id,
|
||||
request = outUserRequestDomain.copy(
|
||||
accepted = true,
|
||||
).toAcceptDTO(),
|
||||
onSuccess = {
|
||||
userRequestDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Обращение успешно принято",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось принять обращение",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDecline = { outUserRequestDomain ->
|
||||
userRequestDomain = outUserRequestDomain
|
||||
confirmCommentDialog = true
|
||||
},
|
||||
)
|
||||
}
|
||||
Models.Role.User -> {
|
||||
userRequestDialogParams = ShowUserRequestDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userRequestDomain = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userRequestDomain?.let { inUserRequestDomain ->
|
||||
userRequestDialogParams?.let { userRequestDialogParams ->
|
||||
UserRequestDialog(
|
||||
userRequestDomain = inUserRequestDomain,
|
||||
userRequestDialogParams = userRequestDialogParams,
|
||||
)
|
||||
}
|
||||
|
||||
if (confirmCommentDialog)
|
||||
ConfirmDialog(
|
||||
confirmDialogParams = ConfirmCommentDialogParams(
|
||||
onDismiss = {
|
||||
confirmCommentDialog = false
|
||||
},
|
||||
onConfirm = { comment ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.accept(
|
||||
id = inUserRequestDomain.id,
|
||||
request = inUserRequestDomain.copy(
|
||||
accepted = false,
|
||||
rejectComment = comment,
|
||||
).toAcceptDTO(),
|
||||
onSuccess = {
|
||||
confirmCommentDialog = false
|
||||
userRequestDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Обращение успешно отклонено",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось отклонить обращение",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
dismissButtonText = "Отмена",
|
||||
confirmButtonText = "Отправить",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.outline_add_24
|
||||
import io.ktor.http.clone
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Config
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.BooleanFilter
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumnList
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.UserCard
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmOnlyDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.CreateUserDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UpdateUserDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserDialogParams
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun UsersScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userDomainList by remember { mutableStateOf(listOf<Models.User.Domain>()) }
|
||||
var municipalityDomainList by remember { mutableStateOf(listOf<Models.Municipality.Domain>()) }
|
||||
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
var userDialogParams by remember { mutableStateOf<UserDialogParams?>(null) }
|
||||
|
||||
var confirmOnlyDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var floatingActionButton by remember { mutableStateOf(true) }
|
||||
|
||||
var deletedFilter by remember { mutableStateOf<Boolean?>(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Municipalities.get(
|
||||
onSuccess = { municipalityDTOList ->
|
||||
municipalityDomainList = municipalityDTOList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список муниципальных образований"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserDomainList() {
|
||||
Api.Users.get(
|
||||
verified = if (deletedFilter != false) null else true,
|
||||
deleted = deletedFilter,
|
||||
onSuccess = { userDTOList ->
|
||||
userDomainList = userDTOList.mapNotNull { userDTO ->
|
||||
when (userDTO.roleId) {
|
||||
1 -> null
|
||||
else -> userDTO.toDomain()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список пользователей",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
userDomain = null
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainScaffoldParams.setFloatingActionButton {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
text = "Добавить пользователя",
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.outline_add_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (municipalityDomainList.isNotEmpty()) {
|
||||
userDialogParams = CreateUserDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onCreate = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.create(
|
||||
user = outUserDomain.toCreateDTO(),
|
||||
onSuccess = {
|
||||
userDomain = null
|
||||
|
||||
val inviteUrl = Config.botUrlBuilder.clone().apply {
|
||||
parameters["startapp"] = it.uuid
|
||||
}.buildString()
|
||||
Platform.copyToClipboard(inviteUrl)
|
||||
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пригласительная ссылка скопирована в буфер обмена",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось добавить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
userDomain = Models.User.Domain(
|
||||
municipality = municipalityDomainList[0],
|
||||
accepted = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
expanded = floatingActionButton,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(mainScaffoldParams.paddingValues),
|
||||
) {
|
||||
BooleanFilter(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
valueMap = mapOf(
|
||||
"Принято" to false,
|
||||
"Все" to null,
|
||||
"Отклонено" to true,
|
||||
),
|
||||
value = deletedFilter,
|
||||
onValueChange = {
|
||||
deletedFilter = it
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MainColumnList(
|
||||
onScrollChange = {
|
||||
floatingActionButton = it
|
||||
},
|
||||
valueList = userDomainList,
|
||||
placeholder = "Список пользователей пуст",
|
||||
) {
|
||||
UserCard(
|
||||
userDomain = it,
|
||||
onClick = {
|
||||
userDialogParams = UpdateUserDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onSave = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.update(
|
||||
user = outUserDomain.toUpdateDTO(),
|
||||
onSuccess = {
|
||||
userDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно изменен",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось изменить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
confirmOnlyDialog = true
|
||||
}
|
||||
)
|
||||
userDomain = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userDomain?.let { inUserDomain ->
|
||||
userDialogParams?.let { userDialogParams ->
|
||||
UserDialog(
|
||||
userDomain = inUserDomain,
|
||||
municipalityDomainList = municipalityDomainList,
|
||||
userDialogParams = userDialogParams,
|
||||
)
|
||||
}
|
||||
|
||||
if (confirmOnlyDialog)
|
||||
ConfirmDialog(
|
||||
confirmDialogParams = ConfirmOnlyDialogParams(
|
||||
onDismiss = {
|
||||
confirmOnlyDialog = false
|
||||
},
|
||||
onConfirm = {
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.delete(
|
||||
id = inUserDomain.id,
|
||||
onSuccess = {
|
||||
confirmOnlyDialog = false
|
||||
userDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно удален",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось удалить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
title = "Удаление пользователя",
|
||||
),
|
||||
dismissButtonText = "Отмена",
|
||||
confirmButtonText = "Удалить",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-RU">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Помощник пчеловода</title>
|
||||
<link type="text/css" rel="stylesheet" href="/styles.css" />
|
||||
<script src="https://telegram.org/js/telegram-web-app.js?57"></script>
|
||||
<script>const getTelegramInitData = () => window.Telegram.WebApp.initData</script>
|
||||
<script type="application/javascript" src="/composeApp.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
html {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
|
||||
body {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
margin: 0 !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
canvas {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
div.blank {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
backdrop-filter: brightness(.5) !important;
|
||||
z-index: 99999 !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
div.container {
|
||||
display: flex !important;
|
||||
gap: 1.5rem !important;
|
||||
flex-direction: column !important;
|
||||
margin: 1rem !important;
|
||||
padding: 1.25rem 1rem !important;
|
||||
width: 100% !important;
|
||||
max-width: 34.25rem !important;
|
||||
border-radius: 1rem !important;
|
||||
background-color: #141218 !important;
|
||||
}
|
||||
|
||||
input.text-field,
|
||||
textarea.text-field {
|
||||
box-sizing: border-box !important;
|
||||
padding: 1.125rem !important;
|
||||
width: 100% !important;
|
||||
border: solid .0625rem #938F99 !important;
|
||||
border-radius: .25rem !important;
|
||||
outline: none !important;
|
||||
background-color: transparent !important;
|
||||
color: #E6E0E9 !important;
|
||||
font-family: 'Roboto', 'Helvetica', 'Arial', 'Verdana', sans-serif !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
input[type="button"] {
|
||||
padding: .875rem !important;
|
||||
font-family: 'Roboto', 'Helvetica', 'Arial', 'Verdana', sans-serif !important;
|
||||
font-size: .875rem !important;
|
||||
font-weight: normal !important;
|
||||
border-radius: 99rem !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
background: #D0BCFF !important;
|
||||
color: #381E72 !important;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#Kotlin
|
||||
kotlin.code.style=official
|
||||
kotlin.daemon.jvmargs=-Xmx3072M
|
||||
|
||||
#Gradle
|
||||
org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
@@ -0,0 +1,51 @@
|
||||
[versions]
|
||||
# Initial
|
||||
androidx-lifecycle = "2.9.1"
|
||||
composeMultiplatform = "1.9.0-alpha03"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.2.10"
|
||||
|
||||
# Custom
|
||||
ktor = "3.2.3"
|
||||
coil = "3.3.0"
|
||||
navigation = "2.9.0-beta04"
|
||||
|
||||
[libraries]
|
||||
# Initial
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||
|
||||
# Custom
|
||||
## Ktor
|
||||
ktor-clientCore = { module = "io.ktor:ktor-client-core-wasm-js", version.ref = "ktor" }
|
||||
ktor-clientContentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-clientLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||
ktor-serializationKotlinxJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
|
||||
## Coil
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-networkKtor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
|
||||
|
||||
# Compose Multiplatform
|
||||
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
|
||||
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
|
||||
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMultiplatform" }
|
||||
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
|
||||
compose-ui-util = { module = "org.jetbrains.compose.ui:ui-util", version.ref = "composeMultiplatform" }
|
||||
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
||||
compose-components-ui-tooling-preview = { module = "org.jetbrains.compose.components:components-ui-tooling-preview", version.ref = "composeMultiplatform" }
|
||||
|
||||
## Navigation
|
||||
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation" }
|
||||
|
||||
[plugins]
|
||||
# Initial
|
||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
|
||||
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
|
||||
# Custom
|
||||
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
||||
Vendored
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,31 @@
|
||||
rootProject.name = "frontend"
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
mavenContent {
|
||||
includeGroupAndSubgroups("androidx")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
includeGroupAndSubgroups("com.google")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google {
|
||||
mavenContent {
|
||||
includeGroupAndSubgroups("androidx")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
includeGroupAndSubgroups("com.google")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
include(":composeApp")
|
||||
Reference in New Issue
Block a user