diff --git a/.gitignore b/.gitignore index e1db807..9f6a2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ local.properties # IntelliJ / Android Studio .idea/ *.iml +*.ipr +*.iws # Kotlin/Java *.class @@ -16,9 +18,91 @@ local.properties # Android *.apk *.aab +*.ap_ +*.dex +*.hprof +*.log + +# Android Studio captures +captures/ + +# External native build +.externalNativeBuild/ # NDK / CMake .cxx/ # OS files .DS_Store + + +app/src/main/raman_res.zip + + +.DS_Store +.idea/shelf +/confluence/target +/dependencies/repo +/android.tests.dependencies +/dependencies/android.tests.dependencies +/dist +/local +/gh-pages +/ideaSDK +/clionSDK +/android-studio/sdk +out/ +/tmp +/intellij +workspace.xml +*.versionsBackup +/idea/testData/debugger/tinyApp/classes* +/jps-plugin/testData/kannotator +/js/js.translator/testData/out/ +/js/js.translator/testData/out-min/ +/js/js.translator/testData/out-pir/ +.gradle/ +build/ +!**/src/**/build +!**/test/**/build +!**/tests-gen/**/build +*.iml +!**/testData/**/*.iml +.idea/artifacts +.idea/remote-targets.xml +.idea/libraries/Gradle*.xml +.idea/libraries/Maven*.xml +.idea/modules +.idea/runConfigurations/JPS_*.xml +.idea/runConfigurations/_JPS_*.xml +.idea/runConfigurations/_FP_*.xml +.idea/runConfigurations/_MT_*.xml +.idea/libraries +.idea/modules.xml +.idea/gradle.xml +.idea/compiler.xml +.idea/inspectionProfiles/profiles_settings.xml +.idea/.name +.idea/jarRepositories.xml +.idea/csv-plugin.xml +.idea/libraries-with-intellij-classes.xml +.idea/misc.xml +.idea/protoeditor.xml +.idea/uiDesigner.xml +.idea/copilot.data.migration.*.xml +node_modules/ +.rpt2_cache/ +local.properties +buildSrcTmp/ +distTmp/ +outTmp/ +/test.output +/kotlin-native/dist +kotlin-ide/ +.kotlin/ +.teamcity/ +.npmrc +/.idea/AndroidProjectSystem.xml +**/.junie/memory/ +.claude/local.md +.claude/CLAUDE.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..effb747 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# Repository Guidelines + +## 프로젝트 구조 및 모듈 구성 +- `app/`: 메인 Android 애플리케이션 모듈. +- `app/src/main/java/`: Kotlin/Java 소스(Compose UI, DI, Room 등). +- `app/src/main/cpp/`: CMake 빌드(`CMakeLists.txt`)를 사용하는 네이티브 코드. +- `app/src/main/res/`: Android 리소스(레이아웃, 드로어블, 문자열). +- `app/src/main/assets/`: 앱 에셋(`raman_res.zip` 포함). +- `app/src/androidTest/java/`: 계측 테스트. +- `build/` 및 `app/build/`: Gradle 빌드 산출물. +- `gradle/` 및 `gradlew`: Gradle wrapper 및 버전 카탈로그. +- `keystore/`: 모듈에서 사용하는 디버그/릴리스 키스토어. + +## 빌드, 테스트 및 개발 명령어 +- `./gradlew build`: 전체 빌드(유닛 테스트 포함). +- `./gradlew assembleDebug`: 디버그 APK 빌드. +- `./gradlew assembleRelease`: 릴리스 APK 빌드(`keystore/` 사용). +- `./gradlew bundleRelease`: 릴리스 AAB 번들 생성. +- `./gradlew installDebug`: 연결된 기기/에뮬레이터에 디버그 빌드 설치. +- `./gradlew lint`: Android Lint 실행. +- `./gradlew connectedAndroidTest`: 기기/에뮬레이터에서 계측 테스트 실행. +- `./gradlew clean`: 빌드 산출물 삭제. + +## 코딩 스타일 및 네이밍 규칙 +- Kotlin/Java: Android 표준 스타일, 4칸 들여쓰기. +- 클래스/Composable: `PascalCase` (예: `MainViewModel`, `SettingsScreen`). +- 함수/변수: `camelCase` (예: `loadProfile`, `isLoading`). +- 리소스: `snake_case` (예: `ic_scan`, `screen_title`). +- 포맷터는 설정되어 있지 않으므로, 주변 코드 스타일에 맞춰 작성. + +## 테스트 가이드 +- 계측 테스트는 `app/src/androidTest/java/`에 위치. +- AndroidX 테스트 러너 + Espresso/Compose 테스트 API 사용. +- `./gradlew connectedAndroidTest`로 실행. + +## 커밋 및 PR 가이드라인 +- 이 워크스페이스에는 Git 히스토리가 없어 강제 커밋 규칙이 없음. +- 간결하고 명령형 커밋 메시지 권장 (예: `Fix scan timeout`). +- PR에는 변경 요약, 테스트 결과, UI 변경 시 스크린샷 포함. + +## 보안 및 설정 팁 +- `local.properties`에는 Android SDK 경로를 지정하며 커밋하지 않음. +- `keystore/`의 키스토어는 로컬 빌드용이며, 프로덕션 배포 시 교체 필요. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b67ac7 --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +# VasCURA589 코드 분석 (com/laseroptek/raman) + +이 문서는 `app/src/main/java/com/laseroptek/raman` 폴더를 기준으로 구조와 동작을 요약한 분석 문서입니다. +요청 경로의 `com/leaseroptek/raman`는 오타로 보이며, 실제 패키지는 `com/laseroptek/raman`입니다. + +## 1) 폴더 개요 + +- 분석 대상 파일 수: **246개** +- 상위 패키지 구성 + - `const` (12): 프로토콜/테이블/상수 + - `data` (57): 모델, DB/Serial/Preference 데이터 소스 + - `di` (4): Hilt DI 모듈 + - `navigation` (5): 라우트/네비게이션 그래프 + - `repository` (3): 데이터 접근 추상화 계층 + - `ui` (123): Compose UI + ViewModel + - `utils` (40): 확장 함수, 보조 유틸, 커스텀 컴포넌트 + +## 2) 아키텍처 요약 + +전체적으로 **Compose + Hilt + Room + DataStore + Serial(NDK)** 구조입니다. + +- Application + - `VasCURA589App`: Hilt 진입점, Timber 로그 트리 설정 +- Activity + - `ui/MainActivity`: 전체 초기화 오케스트레이션, 풀스크린/시스템바 제어, APK 선택 이벤트 처리 +- Presentation + - `ui/screens/...`: Home, Info, Config, Lock, Engineer 화면 + - 공용 상태는 `MainViewModel`이 중심으로 보유 +- Domain/Data Access + - `repository/*Repository`: Serial/Preference/DB 접근 래퍼 +- Data Source + - `data/source/serial/SerialPort`: native `serial_port` 라이브러리 로딩 + FD 기반 통신 + - `data/source/db/*`: Room DB(`SerialLog`) 접근 + - `PreferenceRepository`: DataStore 기반 앱 설정/테이블/카운트 저장 + +## 3) 실행 흐름 (부팅~운영) + +1. `MainActivity.onCreate()` +2. `initialize()`에서 `MainViewModel.performFullInitialization()` 실행 +3. 초기화 완료 후 UI 활성화(`setInitialized(true)`) +4. 시리얼 시작 + - `txPacketOnce()`: 초기 설정 패킷 송신 + - `rxPacketLoop()`: 수신 루프 + 패킷 파싱/상태 반영 + - `txPacketLoop()`: 주기적 heartbeat 성격 송신 + +핵심 포인트: +- 초기화는 IO 스레드에서 병렬 로딩(`launch + joinAll`)로 처리 +- `MainScreen`은 `isInitialized`를 기준으로 로딩 화면 -> 본 화면 전환 +- 통신 타임아웃 감시(`monitorConnectionTimeout`)로 안정성 상태 추적 + +## 4) 주요 패키지별 역할 + +### `ui/screens/main` +- `MainViewModel`: 앱의 사실상 중앙 상태 저장소 + - 레이저 파라미터(펄스/플루언스/반복률), 카운터, 경고/에러, 온도, DCD, 에너지 디텍터 상태 관리 + - 시리얼 TX/RX 패킷 생성/파싱 + - Preference 로드/저장, DB 로그 적재/트리밍 + - 핸드피스 타입 변경 시 에너지/Hz 테이블 재적용 + +### `ui/screens/home` (30개) +- 실제 시술/운영 메인 UI +- 슬라이더 기반 파라미터 조절, 스탠바이/발진, 카운트/프리셋/DCD 제어 + +### `ui/screens/info` (6개) +- 모니터링/차트 화면 +- `InfoViewModel`은 차트 라인 표시 상태(체크박스)만 관리 +- 실데이터(온도, 램프카운트 등)는 `MainViewModel`에서 수신 + +### `ui/screens/config` (7개) +- 볼륨, 가이드빔, 언어/시간 설정, 엔지니어 모드 진입 지점 + +### `ui/screens/engineer` (38개) +- 서비스/캘리브레이션/로그/버전/임계치/시리얼번호 등 유지보수 기능 +- Voltage Table/수명/온도 한계/Energy detect 기준값 조정 + +### `ui/screens/lock` (3개) +- PIN 기반 잠금/해제 플로우 + +## 5) 시리얼 통신 구조 + +- 송신: `MainViewModel.txPacket()` + - `READ_WRITE` + `CMD`로 패킷 조합 + - WRITE 시 CS(checksum) 포함 +- 수신: `rxPacketLoop()` + - 버퍼 누적 후 `findCompletePackets()`로 STX(0x21)~ETX(0x0d) 패킷 분리 + - `procRxPacket()`에서 CMD별 모델 변환 및 상태 업데이트 + +처리 예: +- `CMD.LASER_STATUS`: 발진 상태 반영, 카운트 증가, 알림음 +- `CMD.HAND_PIECE`: 타입 변경 감지 후 테이블/각도/상태 재초기화 +- `CMD.TEMPERATURE`: 차트 큐 갱신 + 임계값 비교 +- `CMD.ENERGY_DETECT`: 버전/마운트/측정 상태 처리 및 Good/Not Good 응답 + +## 6) 저장 전략 + +- **DataStore (`PreferenceRepository`)** + - 카운트, 볼륨, 가이드빔, 프리셋, Spray DCD, Voltage Table, 임계값, 시리얼 번호 리스트 등 +- **Room (`RamanDatabase`, `SerialLogDao`)** + - 주요 시리얼 패킷 로그 저장 + - `trimLogs(limit)`로 로그 최대 개수 유지 + +## 7) 눈에 띄는 구현 특성 + +- `MainViewModel`에 상태/로직이 집중된 구조(단일 허브) +- 디버그 로그(Timber)와 패킷 헥사 덤프 활용 +- 미리보기/테스트를 위한 fake serial repository 제공 +- 패키지 네이밍이 일부 혼재: + - 예: 파일 경로는 `data/source/db`인데 패키지 선언은 `data.datasource.db`인 클래스가 존재 + +## 8) 빠른 참고 파일 + +- 앱 진입: `VasCURA589App.kt`, `ui/MainActivity.kt` +- 내비게이션: `navigation/Routes.kt`, `navigation/graphs/MainNavGraph.kt` +- 핵심 로직: `ui/screens/main/MainViewModel.kt` +- 시리얼: `data/source/serial/SerialPort.kt`, `repository/SerialPortRepository.kt` +- 설정 저장: `repository/PreferenceRepository.kt` +- 로그 DB: `data/source/db/*`, `repository/DatabaseRepository.kt` + +## 9) MainViewModel.kt 별도 분석 + +### 개요 + +- 파일: `app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt` +- 규모: 약 **2,288 라인** +- 역할: 앱 전체의 공용 상태 + 시리얼 프로토콜 처리 + 로컬 저장소 동기화를 한곳에서 담당하는 **중앙 오케스트레이터** + +### 의존성 주입 구조 + +- `PreferenceRepository`: DataStore 기반 영속값 로드/저장 +- `SerialPortRepository`: 시리얼 포트 open/write/close +- `DatabaseRepository`: 시리얼 로그 조회/저장/트리밍 +- `DispatcherProvider`: 코루틴 디스패처 추상화 +- `Context`: 알림음 재생 및 시스템 리소스 접근 + +### 상태(State) 도메인 분류 + +- UI/초기화 제어 + - `isInitialized`, 팝업 노출 플래그, `apkUpdateEvent` +- 레이저 파라미터 + - `pulseAngle`, `fluenceAngle`, `repetitionAngle` + - `energyTable`, `hzTable`, `voltageTable`, `fluenceList`, `repetitionList` +- 프리셋/옵션 + - `presetList`, `selectedPresetIndex` + - `sprayDcdList`, `selectedSprayDcdIndex` +- 카운터/수명 + - `laserCount`, `lampCount`, `dcdCount`, `hpCount`, `lifeTime`, `opTimeHour` +- 장비 상태 패킷 + - `laserStatus`, `handPiece`, `warning`, `error`, `version` + - `temperature`, `temperature_write`, `qSwitch`, `dcdGas`, `sprayDcd`, `oven`, `purgeBubble` +- 에너지 디텍션 + - `energyVersion`, `energyHandpiece`, `energyControl`, `energyMeasured`, `energyMeasuredWrite`, `energyDetectRefer2` +- 모니터링/로그 + - `chartDataQueue`, `serialLogList`, `isCommunicationStable` + +### 핵심 메서드 흐름 + +- 초기화 + - `performFullInitialization()` + - DB 정리/온도 이력 로딩 후, 다수의 preference 로딩을 `launch + joinAll`로 병렬 수행 +- 시리얼 송신 + - `txPacketOnce()`: 버전/QSwitch/GuideBeam/DCD/Spray 기본값 송신 + - `txPacketLoop()`: 주기적 명령(핸드피스, 샷카운트, 경고, 온도) 폴링 + - `txPacket()`: CMD/READ_WRITE에 따라 실제 프로토콜 패킷 생성 +- 시리얼 수신 + - `rxPacketLoop()`: 포트 수신 collect + - `findCompletePackets()`: 버퍼에서 STX~ETX 완성 패킷 분리 + - `procRxPacket()`: CMD 별 디코딩/상태 반영/로그 저장 + - `monitorConnectionTimeout()`: 응답 지연 watchdog + +### 비즈니스 로직 포인트 + +- `CMD.HAND_PIECE` 수신 시 + - 핸드피스 변경 여부 확인 후 레이저 상태/카운트/테이블/슬라이더 초기화 + - handpiece type별 lifetime 카운트 누적 +- `CMD.LASER_STATUS` 수신 시 + - 레이저 ON 상태에서 샷/램프/HP/DCD 카운트 갱신 + - 조건에 따라 알림음 재생 및 스탠바이 전환 처리 +- `CMD.TEMPERATURE` 수신 시 + - 차트 큐에 타임스탬프 데이터 적재 + - 설정 임계값(`temperature_write`)과 비교해 경고/에러 조건 반영 +- `CMD.ENERGY_DETECT` 수신 시 + - measured 값과 refer2 기준 비교 후 Good(0x47)/Not Good(0x4E) 응답 송신 + +### 파라미터 계산 로직 + +- `txLaserStatusEntry(laserStatus: Int)` + - 현재 각도 -> step 변환 + - step -> pulse/fluence/repetition 매핑 + - `(pulse, fluence)`로 energy 조회 + - `calculateInterpolatedC()`로 목표 전압 계산(선형 보간) + - 최종 `LaserStatus` 패킷 송신 + +### 저장소 동기화 패턴 + +- 상태 변경 직후 `save*ToPreference()` 호출하는 즉시 반영형 패턴 +- 앱 시작 시 `load*FromPreference()`로 메모리 상태 복원 +- 시리얼 로그는 특정 CMD만 DB에 적재하고, `trimSerialLog()`로 최대 개수 유지 + +### 설계 관점 평가 + +- 장점 + - 장비 프로토콜 처리, UI 상태, 저장소 동기화가 한 ViewModel에서 일관되게 연결됨 + - 초기화 병렬화로 부팅 지연 완화 + - 통신 watchdog/버퍼 파싱 등 실장비 대응 로직이 명확함 +- 트레이드오프 + - 단일 ViewModel 책임이 매우 큼(상태/함수 밀집) + - 기능 분리(예: SerialHandler, PreferenceSync, LaserDomain) 여지가 큼 + +### 유지보수 시 우선 확인 포인트 + +- 신규 CMD 추가 시 + - `txPacket()` 인코딩 + `procRxPacket()` 디코딩을 함께 확장 +- 파라미터 표 변경 시 + - `loadFluenceTable()`, `loadHzTable()`, `calculateInterpolatedC()` 영향 확인 +- 성능/안정성 이슈 시 + - `txPacketLoop()` 주기, `RX_TIMEOUT_THRESHOLD`, DB 로그 적재량 점검 diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..2058f5b --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,222 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) + alias(libs.plugins.compose) +} + +android { + compileSdk = libs.versions.compileSdk.get().toInt() + namespace = "com.laseroptek.raman" + + defaultConfig { + applicationId = "com.laseroptek.raman" + + versionCode = 173 + versionName = "1.7.3" + + minSdk = libs.versions.minSdk.get().toInt() + maxSdk = libs.versions.maxSdk.get().toInt() + //noinspection ExpiredTargetSdkVersion + targetSdk = libs.versions.targetSdk.get().toInt() + + vectorDrawables.useSupportLibrary = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters += "arm64-v8a" + } + } + + signingConfigs { + // Important: change the keystore for a production deployment + //val userKeystore = File(System.getProperty("user.home"), ".android/debug.keystore") + val localKeystore = rootProject.file("keystore/platform.keystore") + //val hasKeyInfo = userKeystore.exists() + getByName("debug") { + storeFile = localKeystore + storePassword = "123456" + keyAlias = "platform" + keyPassword = "123456" + } + create("release") { + storeFile = localKeystore + storePassword = "123456" + keyAlias = "platform" + keyPassword = "123456" + } + } + + buildTypes { + getByName("debug") { + isMinifyEnabled = false + isDebuggable = true + signingConfig = signingConfigs.getByName("debug") + } + getByName("release") { + isMinifyEnabled = false // default is true but wait too much build time + isDebuggable = false + signingConfig = signingConfigs.getByName("release") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + buildConfig = true + } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + + packaging { + resources { + excludes += "/META-INF/AL2.0" + excludes += "/META-INF/LGPL2.1" + } + jniLibs { + useLegacyPackaging = false + } + } + ndkVersion = "27.0.12077973" + + lint { + disable.add("NullSafeMutableLiveData") + } +} + +kotlin { + jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") + } +} + +dependencies { + // Kotlin and Coroutines + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.android) + + // Dependency injection + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + ksp(libs.hilt.android.compiler) + + // Database + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + + // Logging + implementation(libs.timber) + + // Compose + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + androidTestImplementation(composeBom) + + // splash + implementation(libs.androidx.core.splashscreen) + + // reflection + implementation(kotlin("reflect")) + + //lottie + implementation(libs.lottie.compose) + implementation(libs.snapper) + + implementation(libs.androidx.compose.animation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.material.iconsExtended) + + // Material Android 12 and above. + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.materialWindow) + implementation(libs.androidx.compose.runtime) + + // Compose BOM + implementation(platform(libs.androidx.compose.bom.v20251001)) + + // Add this line + implementation(libs.androidx.foundation) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.activity.compose) + implementation(libs.accompanist.swiperefresh) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.ktx) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.security.crypto.ktx) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.window) + implementation(libs.androidx.localbroadcastmanager) + implementation(libs.androidx.glance) + implementation(libs.androidx.glance.appwidget) + implementation(libs.androidx.glance.material3) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.viewmodel.savedstate) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewModelCompose) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.snapper) + implementation(libs.gson) + + // json binary serialization (completely avoids the "double serialization" problem because there's only one serialization step.) + implementation(libs.kotlinx.serialization.json) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.6.3") + + // repeatOnLifecycle + implementation(libs.lifecycle.runtime.ktx.v262) + + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) + + androidTestImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.androidx.compose.ui.test) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + + //implementation(libs.accompanist.systemuicontroller) + //accompanist System UI Controller (immerssive mode) + + // LockView + //implementation(libs.androidx.constraintlayout.compose) + + // Wheel Picker + //implementation(libs.kmp.date.time.picker) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..c3e6827 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,132 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +-renamesourcefileattribute SourceFile + +# Repackage classes into the top-level. +-repackageclasses + +# This is generated automatically by the Android Gradle plugin. +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.conscrypt.ConscryptHostnameVerifier +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE + + +# JNI (for native C++ code) - More Robust Rules +-keepclasseswithmembers,allowshrinking class * { + native ; +} +-keep class *JNI* { *; } + + +# Hilt / Dagger - Updated Rules +-keep class dagger.hilt.internal.aggregatedroot.codegen.* { (); } +-keep class hilt_aggregated_deps.** { (); } +-keep @dagger.hilt.InstallIn class * +-keep @dagger.hilt.codegen.OriginatingElement class * +-keep @dagger.hilt.android.HiltAndroidApp class * +-keep @dagger.hilt.android.WithFragmentBindings class * +-keep @dagger.assisted.Assisted class * +-keep @dagger.assisted.AssistedFactory class * +-keep @dagger.assisted.AssistedInject class * +-keepclassmembers class * { @dagger.assisted.AssistedInject (...); } + +# Keep Dagger-generated component implementations. +-keep class *_*_HiltComponents_* {} + +# Keep application's modules. +-keep @dagger.Module class * { ; } + +# Keep bindings and their constructors. +-keep @javax.inject.Inject class * { (...); ; } +-keep @javax.inject.Singleton class * +-keepclassmembers class * { @javax.inject.Inject (...); @javax.inject.Inject ; } + +# Keep Parcelable creators +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +# Keep Serializable classes +-keep class * implements java.io.Serializable { *; } + + +# Gson (for data/model classes that are serialized/deserialized) +-keep class com.google.gson.Gson +-keep class com.google.gson.GsonBuilder +-keep class com.google.gson.reflect.TypeToken +-keepclassmembers class com.laseroptek.raman.data.model.** { *; } +-keepattributes Signature +-keepattributes *Annotation* + +# Coroutines & StateFlow +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory +-keepnames class kotlinx.coroutines.flow.** +-keepclassmembers class kotlinx.coroutines.flow.** { *; } +-keep class kotlinx.coroutines.flow.StateFlowImpl { *; } + + +# Room +-keep class androidx.room.RoomDatabase +-keepclassmembers class * extends androidx.room.RoomDatabase { + (...); + java.lang.Object Companion; +} + +# Lottie +-keep class com.airbnb.lottie.** { *; } + +# Glance App Widget +-keepclasseswithmembers public class * extends androidx.glance.appwidget.GlanceAppWidgetReceiver +-keepclasseswithmembers public class * extends androidx.glance.appwidget.GlanceAppWidget +-keep public class * implements androidx.glance.state.GlanceStateDefinition { *; } + +# AndroidX Security Crypto +-keep class androidx.security.crypto.** { *; } + +# ConstraintLayout Compose +-keep public class androidx.constraintlayout.** { *; } + +# Snapper (from Chris Banes) +-keep class dev.chrisbanes.snapper.** { *; } + +# Jetpack Compose +-keep class androidx.compose.runtime.** { *; } +-keepclasseswithmembers public class * { + @androidx.compose.runtime.Composable ; +} +-keepclassmembers class **.R$* { + public static ; +} + +-keep class androidx.compose.ui.platform.AndroidCompositionLocals_androidKt { *; } + +# Keep the SerialPort class and its members from being obfuscated or removed, +# as they are accessed from native code (JNI). +-keep class com.laseroptek.raman.data.source.serial.SerialPort { + ; + ; +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/laseroptek/raman/HomeScreenTests.kt b/app/src/androidTest/java/com/laseroptek/raman/HomeScreenTests.kt new file mode 100644 index 0000000..319055f --- /dev/null +++ b/app/src/androidTest/java/com/laseroptek/raman/HomeScreenTests.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * 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. + */ + +package com.laseroptek.raman + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HomeScreenTests { + + @get:Rule + val composeTestRule = createComposeRule() + + /** + * Checks that the Snackbar is shown when the HomeScreen data contains an error. + */ + @Test + fun postsContainError_snackbarShown() { + val snackbarHostState = SnackbarHostState() + composeTestRule.setContent { + } + + // Then the first message received in the Snackbar is an error message + runBlocking { + // snapshotFlow converts a State to a Kotlin Flow so we can observe it + // wait for the first a non-null `currentSnackbarData` + val actualSnackbarText = snapshotFlow { snackbarHostState.currentSnackbarData } + .filterNotNull().first().visuals.message + val expectedSnackbarText = InstrumentationRegistry.getInstrumentation() + .targetContext.resources.getString(R.string.load_error) + assertEquals(expectedSnackbarText, actualSnackbarText) + } + } +} diff --git a/app/src/androidTest/java/com/laseroptek/raman/TestHelper.kt b/app/src/androidTest/java/com/laseroptek/raman/TestHelper.kt new file mode 100644 index 0000000..e1adc63 --- /dev/null +++ b/app/src/androidTest/java/com/laseroptek/raman/TestHelper.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * 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. + */ + +package com.laseroptek.raman + +import android.content.Context +import androidx.compose.ui.test.junit4.ComposeContentTestRule + +/** + * Launches the app from a test context + */ +fun ComposeContentTestRule.launchramanAppApp(context: Context) { + setContent { + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3639628 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/bubble.json b/app/src/main/assets/bubble.json new file mode 100644 index 0000000..2f5cdf0 --- /dev/null +++ b/app/src/main/assets/bubble.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.21","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":110,"w":600,"h":600,"nm":"petille","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"konf","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[800,800,0],"ix":2},"a":{"a":0,"k":[800,800,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1600,"h":1600,"ip":55,"op":110,"st":55,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"konf","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[800,800,0],"ix":2},"a":{"a":0,"k":[800,800,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"w":1600,"h":1600,"ip":0,"op":110,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"konf","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[800,800,0],"ix":2},"a":{"a":0,"k":[800,800,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1600,"h":1600,"ip":0,"op":110,"st":-55,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 3","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":240,"ix":10},"p":{"a":0,"k":[800,820,0],"ix":2},"a":{"a":0,"k":[800,800,0],"ix":1},"s":{"a":0,"k":[90,90,100],"ix":6}},"ao":0,"w":1600,"h":1600,"ip":20,"op":620,"st":20,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 3","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":120,"ix":10},"p":{"a":0,"k":[800,800,0],"ix":2},"a":{"a":0,"k":[800,800,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"w":1600,"h":1600,"ip":10,"op":610,"st":10,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"Pre-comp 3","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[780,800,0],"ix":2},"a":{"a":0,"k":[800,800,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1600,"h":1600,"ip":0,"op":600,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-27.487]},"t":36,"s":[800]},{"t":76,"s":[796]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[0.858]},"t":36,"s":[800]},{"t":76,"s":[558]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":36,"op":636,"st":36,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[0.67]},"t":34,"s":[800]},{"t":74,"s":[964]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[-0.911]},"t":34,"s":[800]},{"t":74,"s":[1028]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":34,"op":634,"st":34,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[0.611]},"t":32,"s":[800]},{"t":72,"s":[980]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[1.348]},"t":32,"s":[800]},{"t":72,"s":[646]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":32,"op":632,"st":32,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-1]},"t":28,"s":[800]},{"t":68,"s":[690]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[-0.97]},"t":28,"s":[800]},{"t":68,"s":[1014]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":28,"op":628,"st":28,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-0.561]},"t":26,"s":[800]},{"t":66,"s":[604]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[2.596]},"t":26,"s":[800]},{"t":66,"s":[720]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":26,"op":626,"st":26,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-1.222]},"t":24,"s":[800]},{"t":64,"s":[710]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[0.571]},"t":24,"s":[800]},{"t":64,"s":[436]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":24,"op":624,"st":24,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-18.325]},"t":20,"s":[800]},{"t":60,"s":[794]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[-0.487]},"t":20,"s":[800]},{"t":60,"s":[1226]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":20,"op":620,"st":20,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[0.28]},"t":18,"s":[800]},{"t":58,"s":[1192]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[1.06]},"t":18,"s":[800]},{"t":58,"s":[604]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":18,"op":618,"st":18,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[0.294]},"t":16,"s":[800]},{"t":56,"s":[1174]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[-0.88]},"t":16,"s":[800]},{"t":56,"s":[1036]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":16,"op":616,"st":16,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[1.309]},"t":12,"s":[800]},{"t":52,"s":[884]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[0.433]},"t":12,"s":[800]},{"t":52,"s":[320]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":12,"op":612,"st":12,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-0.487]},"t":10,"s":[800]},{"t":50,"s":[574]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[-0.552]},"t":10,"s":[800]},{"t":50,"s":[1176]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":10,"op":610,"st":10,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-0.443]},"t":8,"s":[800]},{"t":48,"s":[552]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[0.538]},"t":8,"s":[800]},{"t":48,"s":[414]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":8,"op":608,"st":8,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[-0.245]},"t":4,"s":[800]},{"t":44,"s":[352]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[-2.884]},"t":4,"s":[800]},{"t":44,"s":[872]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":4,"op":604,"st":4,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[0.524]},"t":2,"s":[800]},{"t":42,"s":[1010]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[-0.517]},"t":2,"s":[800]},{"t":42,"s":[1202]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":2,"op":602,"st":2,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"konfElement","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.297],"y":[1]},"o":{"x":[0.029],"y":[0.591]},"t":0,"s":[800]},{"t":40,"s":[986]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.322],"y":[1]},"o":{"x":[0.008],"y":[0.645]},"t":0,"s":[800]},{"t":40,"s":[478]}],"ix":4}},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":0,"op":600,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[407.219,408.219,0],"ix":2},"a":{"a":0,"k":[7.219,8.219,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[18.438,18.438],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.49411764705882355,0.8274509803921568,0.12941176470588237,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.022],"y":[0.756]},"t":36,"s":[12]},{"t":60,"s":[0]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[7.219,8.219],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.046,0.046],"y":[0.435,0.435]},"t":36,"s":[75,75]},{"t":60,"s":[500,500]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-1,"op":599,"st":-1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Check 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[306,300,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-80.45,12.619],[-35.23,57.839],[80.45,-57.841]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.141176477075,0.662745118141,0.607843160629,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":601,"op":601,"st":0,"bm":0,"hidden":0},{"ddd":0,"ind":2,"ty":4,"nm":"Check 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[306,306,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-80.45,12.619],[-35.23,57.839],[80.45,-57.841]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":60,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":601,"op":601,"st":0,"bm":0,"hidden":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[304.76,304.76,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[18.88,18.88,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[374,374],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.729,0.933,0.098,0.5,0.587,0.797,0.06,1,0.444,0.661,0.022],"ix":9}},"s":{"a":0,"k":[0,-185],"ix":5},"e":{"a":0,"k":[0,250],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":601,"op":601,"st":0,"bm":0,"hidden":601},{"ddd":0,"ind":4,"ty":4,"nm":"Oval 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[305.76,318.76,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[18.88,18.88,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[374,374],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0,0,0,0.5,0,0,0,1,0,0,0,0.268,1,0.634,0.5,1,0],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[180,0],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[342,342],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":601,"op":601,"st":0,"bm":0,"hidden":0},{"ddd":0,"ind":5,"ty":0,"nm":"KonfLoop","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2},"a":{"a":0,"k":[800,800,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"w":1600,"h":1600,"ip":0,"op":110,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/assets/heating.json b/app/src/main/assets/heating.json new file mode 100644 index 0000000..2370e38 --- /dev/null +++ b/app/src/main/assets/heating.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.4.5","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":120,"w":500,"h":500,"nm":"Heat meter Lottie JSON animation","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[377.991,229.123,0],"ix":2},"a":{"a":0,"k":[377.991,229.123,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6.667,"s":[120,120,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13.333,"s":[90,90,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[110,110,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":26.667,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":33.333,"s":[105,105,100]},{"t":40,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.847,0],[0.015,0.002],[1.71,0.422],[4.764,-1.499],[0.261,0.829],[-0.829,0.261],[-3.733,-0.927],[-0.979,-0.032],[0.028,-0.869]],"o":[[-0.015,0],[-1.314,-0.041],[-3.564,-0.882],[-0.823,0.264],[-0.261,-0.829],[5.599,-1.763],[1.538,0.379],[0.869,0.028],[-0.028,0.85]],"v":[[8.889,2.598],[8.839,2.596],[4.339,1.767],[-8.421,1.285],[-10.388,0.257],[-9.36,-1.714],[5.094,-1.284],[8.938,-0.544],[10.46,1.077]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[368.701,257.732],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.226,0],[3.241,1.019],[-0.261,0.827],[-0.835,-0.255],[-5.252,1.544],[-0.255,-0.081],[0.267,-0.826],[0.823,0.27],[2.075,-0.613]],"o":[[-4.709,0],[-0.826,-0.261],[0.258,-0.829],[5.016,1.579],[3.029,-0.895],[0.826,0.268],[-0.264,0.821],[-0.046,-0.012],[-2.419,0.712]],"v":[[-0.293,2.373],[-13.167,0.697],[-14.192,-1.274],[-12.222,-2.302],[7.589,-1.662],[13.179,-1.776],[14.189,0.206],[12.221,1.217],[8.479,1.352]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[367.106,246.933],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.771,0],[0,0],[0,0.772],[-0.77,0],[0,0],[0,-0.772]],"o":[[0,0],[-0.77,0],[0,-0.772],[0,0],[0.771,0],[0,0.772]],"v":[[4.291,1.398],[-4.291,1.398],[-5.688,0],[-4.291,-1.398],[4.291,-1.398],[5.688,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[397.453,231.743],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.771,0],[0,0],[0,0.772],[-0.77,0],[0,0],[0,-0.772]],"o":[[0,0],[-0.77,0],[0,-0.772],[0,0],[0.771,0],[0,0.772]],"v":[[4.291,1.398],[-4.291,1.398],[-5.688,0],[-4.291,-1.398],[4.291,-1.398],[5.688,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[397.453,219.404],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.771,0],[0,0],[0,0.772],[-0.77,0],[0,0],[0,-0.772]],"o":[[0,0],[-0.77,0],[0,-0.772],[0,0],[0.771,0],[0,0.772]],"v":[[4.291,1.398],[-4.291,1.398],[-5.688,0],[-4.291,-1.398],[4.291,-1.398],[5.688,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[397.453,207.064],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-4.954],[4.957,0],[0,4.954],[-4.957,0]],"o":[[0,4.954],[-4.957,0],[0,-4.954],[4.957,0]],"v":[[8.977,0.002],[-0.001,8.974],[-8.977,0.002],[-0.001,-8.974]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[391.343,254.349],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.868,0],[0,0.868],[0,0],[-0.869,0],[0,-0.869],[0,0]],"o":[[-0.869,0],[0,0],[0,-0.869],[0.868,0],[0,0],[0,0.868]],"v":[[0,30.499],[-1.572,28.927],[-1.572,-28.927],[0,-30.499],[1.572,-28.927],[1.572,28.927]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[391.341,225.423],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[250,250,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.285,-0.488],[-0.632,0],[-0.289,0.491],[0,0.801],[0,0],[0.289,0.497],[0.623,0],[0.289,-0.5],[0,-0.783]],"o":[[0,0.807],[0.282,0.491],[0.617,0],[0.285,-0.491],[0,0],[0,-0.786],[-0.291,-0.497],[-0.617,0],[-0.286,0.5],[0,0]],"v":[[-1.799,0.086],[-1.372,2.029],[0.003,2.763],[1.36,2.026],[1.789,0.086],[1.789,-0.086],[1.357,-2.011],[-0.012,-2.756],[-1.369,-2.007],[-1.799,-0.086]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.737,0.844],[-1.304,0],[-0.743,-0.841],[0,-1.329],[0,0],[0.743,-0.838],[1.304,0],[0.743,0.838],[0,1.332]],"o":[[0,-1.323],[0.737,-0.844],[1.311,0],[0.743,0.838],[0,0],[0,1.332],[-0.74,0.838],[-1.311,0],[-0.74,-0.838],[0,0]],"v":[[-4.181,-0.086],[-3.076,-3.334],[-0.012,-4.598],[3.067,-3.337],[4.181,-0.086],[4.181,0.086],[3.07,3.34],[0.003,4.598],[-3.072,3.34],[-4.181,0.086]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[445.819,232.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[1.793,-1.621],[3.155,0],[1.977,2.069],[0,3.278],[0,0],[-1.928,2.081],[-3.131,0],[-1.842,-1.596],[0.049,-2.898],[0,0],[0,0],[0.829,0.841],[1.731,0],[0.902,-1.295],[0,-2.051],[0,0],[-0.951,-1.301],[-1.694,0],[-0.792,0.841],[0,1.744]],"o":[[0,0],[0.049,2.861],[-1.793,1.621],[-3.217,0],[-1.977,-2.069],[0,0],[0,-3.266],[1.927,-2.081],[3.266,0],[1.842,1.596],[0,0],[0,0],[0,-1.719],[-0.829,-0.841],[-1.608,0],[-0.902,1.295],[0,0],[0,2.063],[0.952,1.302],[1.608,0],[0.792,-0.841],[0,0]],"v":[[10.341,4.53],[10.377,4.641],[7.762,11.363],[0.34,13.794],[-7.451,10.691],[-10.416,2.671],[-10.416,-2.652],[-7.524,-10.673],[0.063,-13.794],[7.725,-11.4],[10.414,-4.66],[10.377,-4.549],[5.147,-4.549],[3.904,-8.389],[0.063,-9.651],[-3.703,-7.708],[-5.056,-2.689],[-5.056,2.671],[-3.629,7.717],[0.34,9.669],[3.94,8.407],[5.128,4.53]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[429.88,250.856],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[429.88,250.856],"ix":2},"a":{"a":0,"k":[429.88,250.856],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.546,0.787],[0.973,0],[0.54,-0.787],[0,-1.432],[0,0],[-0.543,-0.783],[-0.992,0],[-0.534,0.783],[0,1.463]],"o":[[0,-1.432],[-0.549,-0.787],[-0.982,0],[-0.534,0.787],[0,0],[0,1.463],[0.543,0.783],[0.973,0],[0.54,-0.783],[0,0]],"v":[[3.092,-3.53],[2.273,-6.861],[-0.008,-8.042],[-2.288,-6.861],[-3.092,-3.53],[-3.092,3.515],[-2.279,6.883],[0.023,8.057],[2.285,6.883],[3.092,3.515]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[1.375,-1.493],[2.271,0],[1.381,1.493],[0,2.62],[0,0],[-1.378,1.504],[-2.281,0],[-1.384,-1.504],[0,-2.609]],"o":[[0,2.62],[-1.378,1.493],[-2.293,0],[-1.381,-1.493],[0,0],[0,-2.609],[1.375,-1.504],[2.272,0],[1.387,1.504],[0,0]],"v":[[7.559,3.085],[5.496,9.255],[0.023,11.495],[-5.487,9.255],[-7.559,3.085],[-7.559,-3.07],[-5.493,-9.239],[-0.008,-11.495],[5.478,-9.239],[7.559,-3.07]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[340.097,104.645],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-4.09,2.855],[1.159,2.855],[1.159,-5.264],[1.067,-5.279],[0.606,-4.282]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[5.64,2.855],[8.157,2.855],[8.157,6.323],[5.64,6.323],[5.64,11.173],[1.159,11.173],[1.159,6.323],[-7.943,6.323],[-8.157,3.637],[1.113,-11.173],[5.64,-11.173]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[322.017,104.645],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[4.543,11.334],[0.077,11.334],[0.077,-7.114],[-4.543,-7.114],[-4.543,-10.536],[4.543,-11.334]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[302.641,104.483],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[302.641,104.483],"ix":2},"a":{"a":0,"k":[302.641,104.483],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.547,0.787],[0.973,0],[0.54,-0.787],[0,-1.432],[0,0],[-0.543,-0.783],[-0.991,0],[-0.534,0.783],[0,1.463]],"o":[[0,-1.432],[-0.549,-0.787],[-0.982,0],[-0.534,0.787],[0,0],[0,1.463],[0.543,0.783],[0.973,0],[0.54,-0.783],[0,0]],"v":[[3.093,-3.53],[2.273,-6.861],[-0.008,-8.042],[-2.288,-6.861],[-3.093,-3.53],[-3.093,3.515],[-2.279,6.883],[0.023,8.057],[2.285,6.883],[3.093,3.515]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[1.375,-1.493],[2.272,0],[1.381,1.493],[0,2.62],[0,0],[-1.378,1.504],[-2.28,0],[-1.385,-1.504],[0,-2.609]],"o":[[0,2.62],[-1.378,1.493],[-2.293,0],[-1.381,-1.493],[0,0],[0,-2.609],[1.375,-1.504],[2.272,0],[1.387,1.504],[0,0]],"v":[[7.559,3.085],[5.496,9.255],[0.023,11.495],[-5.487,9.255],[-7.559,3.085],[-7.559,-3.07],[-5.493,-9.239],[-0.008,-11.495],[5.478,-9.239],[7.559,-3.07]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[131.239,191.608],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.951,0],[-0.553,0.296],[-0.328,0.502],[0,0],[0.574,0.741],[0.961,0],[0.553,-0.833],[0,-1.217],[-0.5,-0.778]],"o":[[0.728,0],[0.553,-0.296],[0,0],[0,-1.401],[-0.574,-0.741],[-0.899,0],[-0.553,0.833],[0,1.278],[0.5,0.778]],"v":[[-0.146,0.537],[1.773,0.092],[3.093,-1.105],[3.093,-3.714],[2.233,-6.93],[-0.069,-8.042],[-2.248,-6.791],[-3.077,-3.714],[-2.325,-0.629]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-0.776,0],[-0.706,0.833],[0,1.493],[0,0],[0.685,-0.327],[0.789,0],[1.179,1.351],[0,2.322],[-1.403,1.474],[-2.18,0],[-1.421,-1.515],[0,-2.681],[0,0],[1.547,-1.515],[2.395,0],[0.823,0.169],[0.728,0.307],[0,0],[-0.614,-0.123]],"o":[[1.105,0],[0.706,-0.834],[0,0],[-0.5,0.645],[-0.685,0.327],[-2.078,0],[-1.185,-1.351],[0,-2.241],[1.403,-1.473],[2.25,0],[1.421,1.515],[0,0],[0,2.578],[-1.553,1.515],[-0.776,0],[-0.826,-0.169],[0,0],[0.645,0.265],[0.614,0.123]],"v":[[-0.683,8.057],[2.034,6.807],[3.093,3.315],[3.093,1.872],[1.312,3.33],[-0.898,3.822],[-5.784,1.796],[-7.559,-3.714],[-5.456,-9.285],[-0.084,-11.495],[5.425,-9.224],[7.559,-2.932],[7.559,3.085],[5.235,9.224],[-0.683,11.495],[-3.083,11.242],[-5.41,10.528],[-4.658,7.29],[-2.77,7.873]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[112.991,191.608],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[112.991,191.608],"ix":2},"a":{"a":0,"k":[112.991,191.608],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.546,0.787],[0.973,0],[0.54,-0.787],[0,-1.432],[0,0],[-0.543,-0.783],[-0.991,0],[-0.534,0.783],[0,1.463]],"o":[[0,-1.432],[-0.549,-0.787],[-0.982,0],[-0.534,0.787],[0,0],[0,1.463],[0.543,0.783],[0.973,0],[0.54,-0.783],[0,0]],"v":[[3.093,-3.53],[2.273,-6.86],[-0.008,-8.042],[-2.289,-6.86],[-3.093,-3.53],[-3.093,3.515],[-2.279,6.884],[0.023,8.058],[2.285,6.884],[3.093,3.515]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[1.375,-1.493],[2.271,0],[1.381,1.493],[0,2.62],[0,0],[-1.378,1.504],[-2.281,0],[-1.384,-1.504],[0,-2.609]],"o":[[0,2.62],[-1.378,1.493],[-2.293,0],[-1.381,-1.493],[0,0],[0,-2.609],[1.375,-1.504],[2.272,0],[1.388,1.504],[0,0]],"v":[[7.559,3.085],[5.496,9.255],[0.023,11.495],[-5.487,9.255],[-7.559,3.085],[-7.559,-3.069],[-5.493,-9.239],[-0.008,-11.495],[5.477,-9.239],[7.559,-3.069]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[75.319,353.091],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[-0.636,0.199],[-0.746,0.02],[-1.176,-1.315],[0,-2.374],[1.268,-1.401],[2.64,0],[1.464,1.09],[-0.052,1.903],[0,0],[0,0],[-0.562,-0.522],[-0.912,0],[-0.525,0.695],[0,1.289],[0.574,0.763],[1.127,0],[0.454,-0.318],[0.193,-0.542]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0.42,-0.307],[0.635,-0.2],[2.158,-0.031],[1.176,1.315],[0,2.19],[-1.268,1.401],[-2.057,0],[-1.464,-1.09],[0,0],[0,0],[0,0.932],[0.562,0.522],[1.145,0],[0.528,-0.695],[0,-1.289],[-0.574,-0.763],[-0.859,0],[-0.458,0.318],[0,0]],"v":[[-7.182,1.159],[-5.892,-11.334],[6.585,-11.334],[6.585,-7.743],[-2.224,-7.743],[-2.838,-2.524],[-1.257,-3.284],[0.815,-3.614],[5.818,-1.688],[7.583,3.845],[5.68,9.232],[-0.183,11.334],[-5.462,9.7],[-7.58,5.211],[-7.55,5.134],[-3.237,4.934],[-2.393,7.114],[-0.183,7.897],[2.325,6.853],[3.117,3.875],[2.258,0.798],[-0.29,-0.345],[-2.261,0.131],[-3.237,1.42]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[57.583,353.252],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[57.583,353.252],"ix":2},"a":{"a":0,"k":[57.583,353.252],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.643,15.642],[6.394,15.642],[6.394,2.643],[-6.351,2.643],[-6.351,15.642],[-12.643,15.642],[-12.643,-15.642],[-6.351,-15.642],[-6.351,-2.192],[6.394,-2.192],[6.394,-15.642],[12.643,-15.642]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[355.062,147.888],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.094,-1.891],[3.68,0],[2.308,2.412],[0,3.825],[0,0],[-2.247,2.425],[-3.653,0],[-2.149,-1.863],[0.058,-3.38],[0,0],[0,0],[0.967,0.979],[2.02,0],[1.053,-1.51],[0,-2.391],[0,0],[-1.108,-1.519],[-1.977,0],[-0.924,0.979],[0,2.035]],"o":[[0,0],[0.058,3.337],[-2.09,1.891],[-3.754,0],[-2.305,-2.416],[0,0],[0,-3.809],[2.25,-2.431],[3.812,0],[2.149,1.863],[0,0],[0,0],[0,-2.004],[-0.967,-0.982],[-1.876,0],[-1.053,1.513],[0,0],[0,2.407],[1.108,1.519],[1.876,0],[0.924,-0.982],[0,0]],"v":[[12.062,5.286],[12.105,5.415],[9.054,13.257],[0.398,16.094],[-8.694,12.475],[-12.15,3.116],[-12.15,-3.094],[-8.78,-12.45],[0.073,-16.094],[9.011,-13.3],[12.148,-5.436],[12.105,-5.307],[6.003,-5.307],[4.554,-9.786],[0.073,-11.259],[-4.32,-8.994],[-5.901,-3.137],[-5.901,3.116],[-4.234,9.003],[0.398,11.281],[4.597,9.81],[5.985,5.286]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[121.143,390.757],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.498,0],[0.184,0.032],[0,0],[-0.304,1.71],[-1.707,-0.307],[0,0],[0.304,-1.71]],"o":[[-0.181,0],[0,0],[-1.71,-0.302],[0.301,-1.71],[0,0],[1.71,0.302],[-0.267,1.524]],"v":[[13.678,5.557],[13.128,5.509],[-14.221,0.684],[-16.769,-2.958],[-13.128,-5.507],[14.221,-0.682],[16.769,2.96]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[121.188,314.276],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.283,0],[0.353,0.13],[0,0],[-0.595,1.631],[-1.627,-0.598],[0,0],[0.595,-1.631]],"o":[[-0.356,0],[0,0],[-1.633,-0.592],[0.592,-1.628],[0,0],[1.633,0.592],[-0.464,1.276]],"v":[[13.049,7.895],[11.974,7.703],[-14.123,-1.797],[-16.002,-5.826],[-11.974,-7.703],[14.123,1.798],[16.002,5.826]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[130.157,280.802],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.087,0],[0.494,0.287],[0,0],[-0.868,1.502],[-1.504,-0.872],[0,0],[0.869,-1.502]],"o":[[-0.534,0],[0,0],[-1.504,-0.867],[0.869,-1.506],[0,0],[1.504,0.867],[-0.583,1.008]],"v":[[12.022,10.089],[10.453,9.667],[-13.597,-4.223],[-14.748,-8.515],[-10.453,-9.665],[13.596,4.225],[14.748,8.517]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[144.804,249.391],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":1,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.712,0],[0.62,0.741],[0,0],[-1.329,1.116],[-1.108,-1.328],[0,0],[1.329,-1.116]],"o":[[-0.899,0],[0,0],[-1.117,-1.329],[1.335,-1.114],[0,0],[1.117,1.329],[-0.589,0.494]],"v":[[8.929,13.78],[6.52,12.657],[-11.333,-8.618],[-10.946,-13.046],[-6.52,-12.658],[11.333,8.617],[10.946,13.045]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[189.188,196.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":1,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.534,0],[0.583,1.008],[0,0],[-1.504,0.869],[-0.872,-1.506],[0,0],[1.504,-0.869]],"o":[[-1.087,0],[0,0],[-0.869,-1.504],[1.498,-0.869],[0,0],[0.868,1.504],[-0.494,0.285]],"v":[[6.946,15.171],[4.22,13.599],[-9.666,-10.454],[-8.515,-14.748],[-4.221,-13.597],[9.666,10.456],[8.515,14.75]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[217.575,176.617],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":1,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.356,0],[0.464,1.277],[0,0],[-1.633,0.592],[-0.592,-1.633],[0,0],[1.633,-0.592]],"o":[[-1.283,0],[0,0],[-0.596,-1.63],[1.624,-0.597],[0,0],[0.596,1.63],[-0.353,0.13]],"v":[[4.75,16.195],[1.797,14.125],[-7.703,-11.976],[-5.825,-16.003],[-1.797,-14.124],[7.703,11.976],[5.825,16.003]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[248.984,161.968],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":1,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.181,0],[0.267,1.524],[0,0],[-1.71,0.302],[-0.301,-1.71],[0,0],[1.71,-0.302]],"o":[[-1.498,0],[0,0],[-0.304,-1.71],[1.71,-0.306],[0,0],[0.304,1.71],[-0.184,0.032]],"v":[[2.409,16.822],[-0.682,14.225],[-5.507,-13.131],[-2.959,-16.773],[0.681,-14.223],[5.507,13.132],[2.959,16.774]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[282.462,152.996],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":1,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.672,-3.42],[3.42,2.672],[0,0],[-2.672,3.42],[-3.42,-2.672],[0,0]],"o":[[-2.672,3.42],[0,0],[-3.42,-2.672],[2.672,-3.42],[0,0],[3.42,2.672]],"v":[[22.713,17.745],[11.683,19.1],[-21.359,-6.715],[-22.713,-17.745],[-11.683,-19.1],[21.359,6.715]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[166.488,222.553],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":1,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-4.34],[4.34,0],[0,0],[0,4.34],[-4.34,0],[0,0]],"o":[[0,4.34],[0,0],[-4.34,0],[0,-4.34],[0,0],[4.34,0]],"v":[[28.823,0],[20.965,7.858],[-20.965,7.858],[-28.823,0],[-20.965,-7.858],[20.965,-7.858]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.866666666667,0.701960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[125.488,348.553],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 15","np":1,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.34,0],[0,4.34],[0,0],[-4.34,0],[0,-4.34],[0,0]],"o":[[-4.34,0],[0,0],[0,-4.34],[4.34,0],[0,0],[0,4.34]],"v":[[0,27.323],[-7.858,19.465],[-7.858,-19.465],[0,-27.323],[7.858,-19.465],[7.858,19.465]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.866666666667,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[316.988,155.553],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":1,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[-13]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[-8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":36,"s":[-13]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[-8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":65,"s":[-13]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":81,"s":[-7]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":97,"s":[-13]},{"t":119,"s":[-90]}],"ix":10},"p":{"a":0,"k":[317.25,349,0],"ix":2},"a":{"a":0,"k":[0,47.022,0],"ix":1},"s":{"a":0,"k":[140,140,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.683,3.768],[0,0],[0,0],[0,0],[0,-10.089],[-13.531,0],[0,13.531]],"o":[[0,0],[0,0],[0,0],[-8.711,3.755],[0,13.531],[13.531,0],[0,-10.069]],"v":[[9.741,24.54],[0.05,-71.522],[0,-71.522],[-9.689,24.516],[-24.5,47.022],[0,71.522],[24.5,47.022]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/assets/laser.json b/app/src/main/assets/laser.json new file mode 100644 index 0000000..12b0729 --- /dev/null +++ b/app/src/main/assets/laser.json @@ -0,0 +1 @@ +{"v":"5.1.15","fr":60,"ip":0,"op":120,"w":314,"h":314,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"barcodeBar Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[75]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[75],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[100],"e":[75]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":90,"s":[75],"e":[100]},{"t":120}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[15,82.826,0],"e":[15,2.826,0],"to":[0,-13.3333330154419,0],"ti":[0,25.6666660308838,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":30,"s":[15,2.826,0],"e":[15,-71.174,0],"to":[0,-25.6666660308838,0],"ti":[0,-1,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":60,"s":[15,-71.174,0],"e":[15,8.826,0],"to":[0,1,0],"ti":[0,-26,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":90,"s":[15,8.826,0],"e":[15,84.826,0],"to":[0,26,0],"ti":[0,-12.6666669845581,0]},{"t":120}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[90,90,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[143.161,6.884],[-143.161,6.884],[-143.161,-6.884],[143.161,-6.884]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.936999990426,0.277999997606,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[160.666,165.055],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"barcodeMain Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[157,157,0],"ix":2},"a":{"a":0,"k":[157,157,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0,31.741],[0,279.333],[313.333,279.333],[313.333,31.741]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"},{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0,31.741],[0,279.333],[313.333,279.333],[313.333,31.741]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 2"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-23.86,-6.753],[23.86,-6.753],[23.86,6.753],[-23.86,6.753]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24.985,38.324],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-2.003,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-23.86,-6.753],[23.86,-6.753],[23.86,6.753],[-23.86,6.753]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[289.473,39.324],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-23.86,-6.753],[23.86,-6.753],[23.86,6.753],[-23.86,6.753]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[289.473,272.58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-23.86,-6.753],[23.86,-6.753],[23.86,6.753],[-23.86,6.753]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24.985,272.58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.753,23.86],[-6.753,23.86],[-6.753,-23.86],[6.753,-23.86]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[6.753,255.473],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.753,23.86],[-6.753,23.86],[-6.753,-23.86],[6.753,-23.86]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[306.58,255.473],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.753,23.86],[-6.753,23.86],[-6.753,-23.86],[6.753,-23.86]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[306.58,55.86],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.753,23.86],[-6.753,23.86],[-6.753,-23.86],[6.753,-23.86]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.838999968884,0.875,0.944999964097,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[6.753,55.86],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[257.444,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[243.359,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.591,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.591,0],[0,0],[0,2.595]],"v":[[0.001,7.042],[-4.695,2.349],[-4.695,-2.347],[0.001,-7.042],[4.694,-2.347],[4.694,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[229.274,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.591,0],[0,-2.596],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.694,2.349],[-4.694,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[215.189,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.591,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.591,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[201.104,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":2,"cix":2,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[187.019,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172.934,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 15","np":2,"cix":2,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.591,0],[0,-2.596],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[158.848,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":2,"cix":2,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[144.763,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 17","np":2,"cix":2,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.591,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.591,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[130.678,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 18","np":2,"cix":2,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.591,0],[0,-2.596],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[116.593,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 19","np":2,"cix":2,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[102.508,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 20","np":2,"cix":2,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.591,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.591,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[88.423,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 21","np":2,"cix":2,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.591,0],[0,-2.596],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[74.337,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 22","np":2,"cix":2,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.595],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.595]],"v":[[0,7.042],[-4.695,2.349],[-4.695,-2.347],[0,-7.042],[4.695,-2.347],[4.695,2.349]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[60.252,232.072],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 23","np":2,"cix":2,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.592,0],[0,-2.597],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[257.444,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 24","np":2,"cix":2,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.591,0],[0,2.596],[0,0],[-2.592,0],[0,-2.597],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.597],[2.591,0],[0,0],[0,2.596]],"v":[[0.001,72.773],[-4.695,68.078],[-4.695,-68.078],[0.001,-72.773],[4.694,-68.078],[4.694,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[229.274,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 25","np":2,"cix":2,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.591,0],[0,-2.597],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.694,68.078],[-4.694,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[215.189,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 26","np":2,"cix":2,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.591,0],[0,2.596],[0,0],[-2.592,0],[0,-2.597],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.597],[2.591,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[191.714,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 27","np":2,"cix":2,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.591,0],[0,-2.597],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[177.628,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 28","np":2,"cix":2,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.592,0],[0,-2.597],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[154.153,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 29","np":2,"cix":2,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.591,0],[0,-2.597],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[125.983,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 30","np":2,"cix":2,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.592,0],[0,-2.597],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[111.898,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 31","np":2,"cix":2,"ix":31,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.591,0],[0,2.596],[0,0],[-2.592,0],[0,-2.597],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.597],[2.591,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[97.813,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 32","np":2,"cix":2,"ix":32,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.591,0],[0,-2.597],[0,0]],"o":[[-2.591,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[74.337,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 33","np":2,"cix":2,"ix":33,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.592,0],[0,-2.597],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.597],[2.592,0],[0,0],[0,2.596]],"v":[[0,72.773],[-4.695,68.078],[-4.695,-68.078],[0,-72.773],[4.695,-68.078],[4.695,68.078]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[60.252,138.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 34","np":2,"cix":2,"ix":34,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.596]],"v":[[0,91.553],[-4.695,86.859],[-4.695,-86.859],[0,-91.553],[4.695,-86.859],[4.695,86.859]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[276.225,156.952],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 35","np":2,"cix":2,"ix":35,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.592,0],[0,2.596],[0,0],[-2.592,0],[0,-2.596],[0,0]],"o":[[-2.592,0],[0,0],[0,-2.596],[2.592,0],[0,0],[0,2.596]],"v":[[0,91.553],[-4.695,86.859],[-4.695,-86.859],[0,-91.553],[4.695,-86.859],[4.695,86.859]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.33300000359,0.375999989229,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.472,156.952],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 36","np":2,"cix":2,"ix":36,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/assets/lightbulb.json b/app/src/main/assets/lightbulb.json new file mode 100644 index 0000000..1916795 --- /dev/null +++ b/app/src/main/assets/lightbulb.json @@ -0,0 +1 @@ +{"v":"5.4.4","fr":29.9700012207031,"ip":0,"op":90.0000036657751,"w":700,"h":700,"nm":"Comp 1","ddd":0,"assets":[{"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Capa de formas 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[356.812,362,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132.18,56.547],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.926087622549,0.926087622549,0.926087622549,1],"ix":4},"o":{"a":0,"k":39,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-11.723,96.273],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[258.781,258.781],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Trazado elíptico 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.926087622549,0.926087622549,0.926087622549,1],"ix":4},"o":{"a":0,"k":39,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-8.812,-41.484],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[107.656,107.656],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":2,"nm":"Combinar trazados 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.926087622549,0.926087622549,0.926087622549,1],"ix":4},"o":{"a":0,"k":39,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Capa de formas 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[350,350,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[39.649,-5.287],[-11.632,0.969],[-10.266,14.839],[0,0],[-13.334,-17.873],[26.874,13.23],[0,2]],"o":[[0,0],[-22.5,3],[12,-1],[10.634,-15.37],[0,0],[23.5,31.5],[-32.5,-16],[0,-2]],"v":[[-15,130],[-40,-18.5],[-41,5.5],[4.866,-16.63],[-1.5,-34],[-13.5,-17],[35,-22.5],[7,129]],"c":false},"ix":2},"nm":"Trazado 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.100245098039,0.100245098039,0.100245098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Trazo 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Forma 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0}],"id":47,"nm":"apagado","xt":1,"w":700,"h":700},{"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Capa de formas 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[348,350,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-56,4]],"o":[[0,0],[56,-4]],"v":[[-92,-54],[-22,-118]],"c":false},"ix":2},"nm":"Trazado 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.958989999809,0.967233455882,0.889445824717,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Trazo 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Forma 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Capa de formas 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[356.812,356,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132.18,56.547],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.947916666667,0.807167741364,0.062744305181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-11.723,96.273],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[258.781,258.781],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Trazado elíptico 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.947916666667,0.807167741364,0.062744305181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-8.812,-41.484],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[107.656,107.656],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":2,"nm":"Combinar trazados 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.947916666667,0.807167741364,0.062744305181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0}],"id":26,"nm":"prendido","xt":1,"w":700,"h":700},{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Capa de formas 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[348,350,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-56,4]],"o":[[0,0],[56,-4]],"v":[[-92,-54],[-22,-118]],"c":false},"ix":2},"nm":"Trazado 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.958989999809,0.967233455882,0.889445824717,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Trazo 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Forma 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Capa de formas 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[356.812,356,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132.18,56.547],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.947916666667,0.807167741364,0.062744305181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-11.723,96.273],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[258.781,258.781],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Trazado elíptico 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.947916666667,0.807167741364,0.062744305181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-8.812,-41.484],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[107.656,107.656],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":2,"nm":"Combinar trazados 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.947916666667,0.807167741364,0.062744305181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Capa de formas 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[356.812,362,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132.18,56.547],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.926087622549,0.926087622549,0.926087622549,1],"ix":4},"o":{"a":0,"k":39,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-11.723,96.273],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[258.781,258.781],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Trazado elíptico 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.926087622549,0.926087622549,0.926087622549,1],"ix":4},"o":{"a":0,"k":39,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-8.812,-41.484],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[107.656,107.656],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":2,"nm":"Combinar trazados 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.926087622549,0.926087622549,0.926087622549,1],"ix":4},"o":{"a":0,"k":39,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Capa de formas 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[350,350,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[39.649,-5.287],[-11.632,0.969],[-10.266,14.839],[0,0],[-13.334,-17.873],[26.874,13.23],[0,2]],"o":[[0,0],[-22.5,3],[12,-1],[10.634,-15.37],[0,0],[23.5,31.5],[-32.5,-16],[0,-2]],"v":[[-15,130],[-40,-18.5],[-41,5.5],[4.866,-16.63],[-1.5,-34],[-13.5,-17],[35,-22.5],[7,129]],"c":false},"ix":2},"nm":"Trazado 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.100245098039,0.100245098039,0.100245098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Trazo 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Forma 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Capa de formas 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[348.5,356,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,91.742,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[166.867,57.758],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.351319316789,0.383957328048,0.396400122549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.566,164.879],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Capa de formas 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[347.177,406,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[69.569,70.066,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[166.867,57.758],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.211764705882,0.250980392157,0.266666666667,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.566,164.879],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Capa de formas 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[346.634,436,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[67.033,67.033,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[166.867,57.758],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.351319316789,0.383957328048,0.396400122549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.566,164.879],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Capa de formas 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[345.787,499,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[39.138,39.138,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[166.867,57.758],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.211764705882,0.250980392157,0.266666666667,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.566,164.879],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Capa de formas 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[346.134,509,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[39.138,39.138,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[166.867,57.758],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.351319316789,0.383957328048,0.396400122549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.566,164.879],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Rectángulo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"prendido","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[100],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[100],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[100],"e":[0]},{"t":85.000003462121}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[350,356,0],"ix":2},"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":700,"h":700,"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"apagado","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[350,350,0],"ix":2},"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":700,"h":700,"ip":0,"op":90.0000036657751,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/assets/lottie.json b/app/src/main/assets/lottie.json new file mode 100644 index 0000000..e099ff1 --- /dev/null +++ b/app/src/main/assets/lottie.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":144,"w":700,"h":700,"nm":"Loader-Vector","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pen ","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.52,"y":0.943},"o":{"x":0.48,"y":0.057},"t":0,"s":[367,336.5,0],"to":[4.167,-9.667,0],"ti":[-4.167,9.667,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":7.5,"s":[392,278.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.978},"o":{"x":0.167,"y":0},"t":16.5,"s":[392,278.5,0],"to":[-24.833,25.833,0],"ti":[24.833,-25.833,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":26.25,"s":[243,433.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.939},"o":{"x":0.167,"y":0},"t":30.75,"s":[243,433.5,0],"to":[-0.167,12.833,0],"ti":[0.167,-12.833,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":40.5,"s":[242,510.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.968},"o":{"x":0.167,"y":0},"t":46.5,"s":[242,510.5,0],"to":[25.167,12,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":57.75,"s":[393,582.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.948},"o":{"x":0.167,"y":0},"t":63,"s":[393,582.5,0],"to":[0,0,0],"ti":[-13.833,0,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":72,"s":[476,582.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.972},"o":{"x":0.167,"y":0},"t":76.5,"s":[476,582.5,0],"to":[11.167,-26,0],"ti":[-11.167,26,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":86.25,"s":[543,426.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.947},"o":{"x":0.167,"y":0},"t":90.75,"s":[543,426.5,0],"to":[-0.167,-13.667,0],"ti":[0.167,13.667,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":99.75,"s":[542,344.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":0.972},"o":{"x":0.167,"y":0},"t":104.25,"s":[542,344.5,0],"to":[-25.167,-11.5,0],"ti":[25.167,11.5,0]},{"i":{"x":0.52,"y":0.52},"o":{"x":0.167,"y":0.167},"t":114,"s":[391,275.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.52,"y":1},"o":{"x":0.167,"y":0},"t":118.5,"s":[391,275.5,0],"to":[-4,10.167,0],"ti":[4,-10.167,0]},{"t":126,"s":[367,336.5,0]}],"ix":2},"a":{"a":0,"k":[60,89.5,0],"ix":1},"s":{"a":0,"k":[94,94,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.039,-2.228],[2.401,-0.049],[0.053,2.276],[-2.297,-0.029]],"o":[[0.044,2.51],[-2.332,0.048],[-0.054,-2.305],[2.17,0.027]],"v":[[4.642,-0.136],[0.202,4.685],[-4.632,0.168],[0.167,-4.704]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[52.02,105.164],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.682,-4.643],[-5.291,-14.405],[1.68,-2.22],[-11.341,-2.06],[-2.047,6.14],[9.945,1.254],[0.46,1.272],[6.088,16.854],[1.043,2.832],[0,0],[-0.547,-0.542],[-17.287,-17.277],[0.015,-1.309],[0.25,-19.713],[1.594,-0.573],[8.693,-3.279],[1.105,1.271],[13.001,15.017],[-0.101,1.168],[-1.218,12.745],[-1.13,11.5],[-0.177,0.788]],"o":[[1.695,4.638],[5.225,14.429],[0.856,2.335],[-6.958,9.191],[6.281,1.142],[2.747,-8.243],[-1.707,-0.215],[-6.085,-16.854],[-1.024,-2.837],[0,0],[0.575,0.513],[17.361,17.203],[0.92,0.919],[-0.217,19.713],[-0.023,1.859],[-8.745,3.145],[-1.601,0.603],[-13.041,-14.985],[-0.75,-0.867],[1.11,-12.755],[1.099,-11.502],[0.079,-0.798],[0,0]],"v":[[-28.383,-60.361],[-23.301,-46.447],[-7.604,-3.165],[-8.365,3.097],[1.026,26.631],[16.978,16.421],[4.793,-2.886],[2.126,-5.358],[-16.125,-55.924],[-19.252,-64.418],[-18.613,-64.825],[-16.891,-63.281],[35.131,-11.611],[36.683,-7.718],[35.84,51.42],[33.448,54.639],[7.271,64.222],[3.606,63.36],[-35.541,18.427],[-36.597,14.853],[-33.036,-23.391],[-29.692,-57.894],[-29.174,-60.26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[48.352,92.59],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.044,2.51],[2.17,0.027],[-0.054,-2.305],[-2.332,0.048]],"o":[[-0.039,-2.228],[-2.297,-0.029],[0.053,2.276],[2.401,-0.049]],"v":[[-3.054,15.689],[-7.528,11.121],[-12.327,15.993],[-7.493,20.509]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.695,4.637],[0,0],[0.078,-0.797],[1.099,-11.503],[1.11,-12.755],[-0.75,-0.866],[-13.041,-14.984],[-1.601,0.604],[-8.744,3.145],[-0.023,1.86],[-0.217,19.713],[0.92,0.92],[17.361,17.204],[0.575,0.513],[0,0],[-1.024,-2.838],[-6.085,-16.854],[-1.707,-0.215],[2.747,-8.242],[6.281,1.141],[-6.958,9.192],[0.856,2.334],[5.225,14.429]],"o":[[0,0],[-0.177,0.788],[-1.131,11.5],[-1.218,12.745],[-0.101,1.168],[13.001,15.018],[1.105,1.271],[8.693,-3.278],[1.594,-0.573],[0.25,-19.712],[0.015,-1.309],[-17.287,-17.277],[-0.547,-0.542],[0,0],[1.043,2.832],[6.088,16.854],[0.46,1.272],[9.945,1.254],[-2.047,6.141],[-11.341,-2.06],[1.68,-2.219],[-5.291,-14.406],[-1.682,-4.643]],"v":[[-39.746,-57.109],[-40.537,-57.008],[-41.054,-54.644],[-44.399,-20.14],[-47.96,18.104],[-46.904,21.678],[-7.757,66.61],[-4.092,67.473],[22.085,57.891],[24.477,54.67],[25.32,-4.466],[23.768,-8.36],[-28.254,-60.031],[-29.976,-61.574],[-30.615,-61.167],[-27.488,-52.672],[-9.237,-2.107],[-6.57,0.364],[5.615,19.671],[-10.337,29.882],[-19.728,6.348],[-18.967,0.086],[-34.664,-43.195]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,-1.937],[-3.005,1.109],[-2.02,0.761],[-1.161,-2.866],[2.924,-1.068],[9.216,-3.315],[18.113,-6.527],[0.767,-0.313],[1.196,2.617],[-2.903,1.168],[-4.417,1.696],[-1.201,0.535],[1.395,1.602],[11.718,13.387],[-0.249,2.62],[-1.125,12.004],[-1.235,13.33],[-0.359,8.796],[-3.643,-1.526],[-0.989,-0.98],[-24.219,-24.006],[0.045,-2.353],[0.312,-18.301]],"o":[[3.58,-1.295],[2.024,-0.746],[2.99,-1.126],[1.006,2.481],[-9.199,3.361],[-18.117,6.518],[-0.78,0.281],[-3.128,1.274],[-1.27,-2.782],[4.387,-1.767],[0.833,-0.32],[-1.555,-1.863],[-11.679,-13.42],[-1.764,-2.015],[1.137,-12.002],[1.249,-13.328],[0.813,-8.777],[0.202,-4.961],[1.251,0.524],[24.217,24.01],[1.758,1.742],[-0.35,18.3],[-0.024,1.452]],"v":[[35.298,53.265],[44.9,49.786],[50.946,47.474],[58.159,50.51],[54.812,57.102],[27.157,67.024],[-27.184,86.604],[-29.533,87.428],[-36.278,85.348],[-33.417,78.057],[-20.163,72.982],[-17.328,71.726],[-21.611,66.606],[-56.684,26.375],[-58.916,19.503],[-55.519,-16.506],[-51.793,-56.494],[-49.508,-82.833],[-41.863,-87.176],[-38.52,-84.644],[34.1,-12.587],[36.266,-6.479],[35.3,48.423]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[59.715,89.339],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":144,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"c1 ","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[349,197,0],"ix":2},"a":{"a":0,"k":[17,17,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[13.302,13.302],[-13.302,13.302],[-13.302,-13.302],[13.302,-13.302]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.99,17.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":114,"s":[87]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":115,"s":[87]},{"t":118,"s":[0]}],"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false}],"ip":8,"op":144,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"bez1 ","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[348.5,197,0],"ix":2},"a":{"a":0,"k":[87.5,8,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.872,0],[0,3.872],[-3.872,0],[0,-3.871]],"o":[[-3.872,0],[0,-3.871],[3.872,0],[0,3.872]],"v":[[0.001,7.01],[-7.01,0],[0.001,-7.01],[7.01,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[7.714,8.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.872,0],[0,3.872],[-3.872,0],[0,-3.871]],"o":[[-3.872,0],[0,-3.871],[3.872,0],[0,3.872]],"v":[[0,7.01],[-7.011,0],[0,-7.01],[7.011,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[167.553,8.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[166.185,8.002],[12.7,8.002]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.7,8.003],[166.183,8.003]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":144,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"c2 ","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197,349.5,0],"ix":2},"a":{"a":0,"k":[17,16.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[13.302,13.302],[-13.302,13.302],[-13.302,-13.302],[13.302,-13.302]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.982,16.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":31,"op":144,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"bez2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197,351.5,0],"ix":2},"a":{"a":0,"k":[8,87.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.871],[3.871,0],[0,3.872],[-3.872,0]],"o":[[0,3.872],[-3.872,0],[0,-3.871],[3.871,0]],"v":[[7.011,-0.001],[0,7.01],[-7.011,-0.001],[0,-7.01]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.52,"y":0.96},"o":{"x":0.48,"y":0.04},"t":30.75,"s":[7.981,93.247],"to":[0,12.387],"ti":[0,-12.387]},{"t":40.5,"s":[7.981,167.567]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"pel2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.872],[3.871,0],[0,3.872],[-3.872,0]],"o":[[0,3.872],[-3.872,0],[0,-3.872],[3.871,0]],"v":[[7.011,-0.001],[0,7.01],[-7.011,-0.001],[0,-7.01]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.52,"y":0.96},"o":{"x":0.48,"y":0.04},"t":30.75,"s":[7.981,79.002],"to":[0,-11.879],"ti":[0,11.879]},{"t":40.5,"s":[7.981,7.728]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"pel1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.52,"y":0.947},"o":{"x":0.48,"y":0.053},"t":30.75,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.963,71.597],[7.963,99.082]],"c":false}]},{"t":40.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.981,9.097],[7.981,162.582]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lin","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":32,"op":144,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"c3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[349,500.5,0],"ix":2},"a":{"a":0,"k":[17,16.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[13.302,13.302],[-13.302,13.302],[-13.302,-13.302],[13.302,-13.302]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.99,16.443],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":58,"op":144,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"bez3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[348,501.5,0],"ix":2},"a":{"a":0,"k":[8,87.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.871],[3.871,0],[0,3.872],[-3.872,0]],"o":[[0,3.872],[-3.872,0],[0,-3.871],[3.871,0]],"v":[[7.011,-0.001],[0,7.01],[-7.011,-0.001],[0,-7.01]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.52,"y":0.963},"o":{"x":0.48,"y":0.037},"t":63,"s":[7.981,93.247],"to":[0,12.387],"ti":[0,-12.387]},{"t":72,"s":[7.981,167.567]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"pel2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.872],[3.871,0],[0,3.872],[-3.872,0]],"o":[[0,3.872],[-3.872,0],[0,-3.872],[3.871,0]],"v":[[7.011,-0.001],[0,7.01],[-7.011,-0.001],[0,-7.01]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.52,"y":0.963},"o":{"x":0.48,"y":0.037},"t":63,"s":[7.981,79.002],"to":[0,-11.879],"ti":[0,11.879]},{"t":72,"s":[7.981,7.728]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"pel1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.52,"y":0.947},"o":{"x":0.48,"y":0.053},"t":63,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.963,71.597],[7.963,99.082]],"c":false}]},{"t":72,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.981,9.097],[7.981,162.582]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lin","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":63,"op":144,"st":43,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"c4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[499.5,347.5,0],"ix":2},"a":{"a":0,"k":[16.5,16.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[13.302,13.302],[-13.302,13.302],[-13.302,-13.302],[13.302,-13.302]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.313,16.79],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":86,"op":144,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"bez4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[500,347.5,0],"ix":2},"a":{"a":0,"k":[8,87.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.871],[3.871,0],[0,3.872],[-3.872,0]],"o":[[0,3.872],[-3.872,0],[0,-3.871],[3.871,0]],"v":[[7.011,-0.001],[0,7.01],[-7.011,-0.001],[0,-7.01]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.52,"y":0.963},"o":{"x":0.48,"y":0.037},"t":90.75,"s":[7.981,93.247],"to":[0,12.387],"ti":[0,-12.387]},{"t":99.75,"s":[7.981,167.567]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"pel2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.872],[3.871,0],[0,3.872],[-3.872,0]],"o":[[0,3.872],[-3.872,0],[0,-3.872],[3.871,0]],"v":[[7.011,-0.001],[0,7.01],[-7.011,-0.001],[0,-7.01]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.52,"y":0.963},"o":{"x":0.48,"y":0.037},"t":90.75,"s":[7.981,79.002],"to":[0,-11.879],"ti":[0,11.879]},{"t":99.75,"s":[7.981,7.728]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"pel1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.52,"y":0.947},"o":{"x":0.48,"y":0.053},"t":90.75,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.963,71.597],[7.963,99.082]],"c":false}]},{"t":99.75,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.981,9.097],[7.981,162.582]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lin","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":144,"st":80,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"circ","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"t":136,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[349,348.5,0],"ix":2},"a":{"a":0,"k":[171,171.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.52,"y":0.947},"o":{"x":0.48,"y":0.053},"t":30.75,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[150.58,0],[0,150.579],[-150.58,0],[0,-150.579]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.48,"y":0},"t":40.5,"s":[{"i":[[0,0],[0,0],[0.945,3.5],[0,0]],"o":[[0,0],[0,0],[9.945,-149.5],[0,0]],"v":[[150.58,0],[0,150.579],[-150.58,0],[0,-150.579]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":63,"s":[{"i":[[0,0],[0,0],[0.945,3.5],[0,0]],"o":[[0,0],[0,0],[9.945,-149.5],[0,0]],"v":[[150.58,0],[0,150.579],[-150.58,0],[0,-150.579]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":72,"s":[{"i":[[0,0],[28.366,-1.079],[8.055,116.5],[-31.366,-6.08]],"o":[[0,0],[-51.634,1.921],[3.945,-121.5],[31.366,6.08]],"v":[[150.58,0],[0,150.579],[-150.58,0],[0,-150.579]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90.75,"s":[{"i":[[0,0],[28.366,-1.079],[8.055,116.5],[-31.366,-6.08]],"o":[[0,0],[-51.634,1.921],[3.945,-121.5],[31.366,6.08]],"v":[[150.58,0],[0,150.579],[-150.58,0],[0,-150.579]],"c":true}]},{"t":99.75,"s":[{"i":[[-10.214,-137.5],[28.366,-1.079],[8.055,116.5],[-31.366,-6.08]],"o":[[-11.214,140.5],[-51.634,1.921],[3.945,-121.5],[36.366,4.08]],"v":[[150.58,0],[0,150.579],[-150.58,0],[0,-150.579]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.161000001197,0.670999983245,0.885999971278,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[170.634,171.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.52],"y":[0.96]},"o":{"x":[0.48],"y":[0.04]},"t":16.5,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":26.25,"s":[74]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":46.5,"s":[74]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":57.75,"s":[49]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":76.5,"s":[49]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":86.25,"s":[26]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":104.25,"s":[26]},{"t":114,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":-92,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":144,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/assets/planes.json b/app/src/main/assets/planes.json new file mode 100644 index 0000000..7d436d7 --- /dev/null +++ b/app/src/main/assets/planes.json @@ -0,0 +1 @@ +{"v":"5.7.4","fr":24,"ip":24,"op":144,"w":1500,"h":650,"nm":"Pre-comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[754.266,325,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24.966,24.966],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-741.783,4.579],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"planes/1. Cover.ai 3","cl":"ai","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":29.495,"ix":10},"p":{"a":0,"k":[-16.729,-7.195,0],"ix":2,"l":2},"a":{"a":0,"k":[41.19,339.788,0],"ix":1,"l":2},"s":{"a":0,"k":[80,80,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[3.569,-6.866],[7.621,1.604],[-7.621,6.866]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[18.549,347.137],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-39.674,-9.397],[-31.072,10],[39.674,-9.999]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.19,322.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-29.373,0.483],[-16.669,27.038],[29.373,-27.038]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[51.491,339.788],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-35.374,-0.627],[-34.562,20.626],[-19.32,15.364],[35.373,-20.626]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[45.491,333.377],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Trace Shape Layer 3: Path 1 [1.1]","cl":"1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10,"x":"var $bm_rt;\nvar pathToTrace = thisComp.layer('Shape Layer 3')('ADBE Root Vectors Group')(1)('ADBE Vectors Group')(1)('ADBE Vector Shape');\nvar progress = $bm_div(thisLayer.effect('Pseudo/ADBE Trace Path')('Pseudo/ADBE Trace Path-0001'), 100);\nvar pathTan = pathToTrace.tangentOnPath(progress);\n$bm_rt = radiansToDegrees(Math.atan2(pathTan[1], pathTan[0]));"},"p":{"a":0,"k":[750,325,0],"ix":2,"x":"var $bm_rt;\nvar pathLayer = thisComp.layer('Shape Layer 3');\nvar progress = $bm_div(thisLayer.effect('Pseudo/ADBE Trace Path')('Pseudo/ADBE Trace Path-0001'), 100);\nvar pathToTrace = pathLayer('ADBE Root Vectors Group')(1)('ADBE Vectors Group')(1)('ADBE Vector Shape');\n$bm_rt = pathLayer.toComp(pathToTrace.pointOnPath(progress));","l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-1180.77,-342.592]],"c":false},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ef":[{"ty":5,"nm":"Trace Path","np":4,"mn":"Pseudo/ADBE Trace Path","ix":1,"en":1,"ef":[{"ty":0,"nm":"Progress","mn":"Pseudo/ADBE Trace Path-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[0.202]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.511]},"t":55,"s":[33.004]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72.429,"s":[62]},{"t":96,"s":[100]}],"ix":1}},{"ty":7,"nm":"Loop","mn":"Pseudo/ADBE Trace Path-0002","ix":2,"v":{"a":0,"k":1,"ix":2}}]}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[750,325,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.0]","np":3,"mn":"ADBE Layer Control","ix":1,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]},{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.1]","np":3,"mn":"ADBE Layer Control","ix":2,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]},{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.2]","np":3,"mn":"ADBE Layer Control","ix":3,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]},{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.3]","np":3,"mn":"ADBE Layer Control","ix":4,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]},{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.4]","np":3,"mn":"ADBE Layer Control","ix":5,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]},{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.5]","np":3,"mn":"ADBE Layer Control","ix":6,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]},{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.6]","np":3,"mn":"ADBE Layer Control","ix":7,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]},{"ty":5,"nm":"Shape Layer 3: Path 1 [1.1.7]","np":3,"mn":"ADBE Layer Control","ix":8,"en":1,"ef":[{"ty":10,"nm":"Layer","mn":"ADBE Layer Control-0001","ix":1,"v":{"a":0,"k":4,"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-61.099,-145.373],[13.542,2.851],[-122.588,27.083],[-53.454,81.963],[-210.242,-2.134],[-13.419,-96.915],[0,0]],"o":[[0,0],[20.669,49.178],[-13.542,-2.851],[122.588,-27.083],[53.454,-81.963],[140.406,1.425],[25.658,185.307],[0,0]],"v":[[-764.967,15.68],[-593.202,80.537],[-606.031,136.842],[-522.643,45.614],[-320.943,40.625],[14.035,-193.86],[272.752,17.105],[401.754,164.638]],"c":false},"ix":2,"x":"var $bm_rt;\nvar nullLayerNames = [\n 'Shape Layer 3: Path 1 [1.1.0]',\n 'Shape Layer 3: Path 1 [1.1.1]',\n 'Shape Layer 3: Path 1 [1.1.2]',\n 'Shape Layer 3: Path 1 [1.1.3]',\n 'Shape Layer 3: Path 1 [1.1.4]',\n 'Shape Layer 3: Path 1 [1.1.5]',\n 'Shape Layer 3: Path 1 [1.1.6]',\n 'Shape Layer 3: Path 1 [1.1.7]'\n ];\nvar origPath = thisProperty;\nvar origPoints = origPath.points();\nvar origInTang = origPath.inTangents();\nvar origOutTang = origPath.outTangents();\nvar getNullLayers = [];\nfor (var i = 0, il = nullLayerNames.length; i < il; i++) {\n try {\n getNullLayers.push(effect(nullLayerNames[i])('ADBE Layer Control-0001'));\n } catch (err) {\n getNullLayers.push(null);\n }\n}\nfor (var i = 0, il = getNullLayers.length; i < il; i++) {\n if (getNullLayers[i] != null && getNullLayers[i].index != thisLayer.index) {\n origPoints[i] = fromCompToSurface(getNullLayers[i].toComp(getNullLayers[i].anchorPoint));\n }\n}\n$bm_rt = createPath(origPoints, origInTang, origOutTang, origPath.isClosed());"},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":8,"ix":1}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Trace Shape Layer 3: Path 1 [1.1]').effect('Trace Path')('Progress');"},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"planes/1. Cover.ai 2","cl":"ai","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":29.49,"ix":10},"p":{"a":0,"k":[-16.71,-7.17,0],"ix":2,"l":2},"a":{"a":0,"k":[41.19,339.788,0],"ix":1,"l":2},"s":{"a":0,"k":[80,80,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[3.569,-6.866],[7.621,1.604],[-7.621,6.866]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[18.549,347.137],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-39.674,-9.397],[-31.072,10],[39.674,-9.999]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.19,322.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-29.373,0.483],[-16.669,27.038],[29.373,-27.038]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[51.491,339.788],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-35.374,-0.627],[-34.562,20.626],[-19.32,15.364],[35.373,-20.626]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[45.491,333.377],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":3,"nm":"Trace Shape Layer 2: Path 1 [1.1]","cl":"1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10,"x":"var $bm_rt;\nvar pathToTrace = thisComp.layer('Shape Layer 2')('ADBE Root Vectors Group')(1)('ADBE Vectors Group')(1)('ADBE Vector Shape');\nvar progress = $bm_div(thisLayer.effect('Pseudo/ADBE Trace Path')('Pseudo/ADBE Trace Path-0001'), 100);\nvar pathTan = pathToTrace.tangentOnPath(progress);\n$bm_rt = radiansToDegrees(Math.atan2(pathTan[1], pathTan[0]));"},"p":{"a":0,"k":[750,325,0],"ix":2,"x":"var $bm_rt;\nvar pathLayer = thisComp.layer('Shape Layer 2');\nvar progress = $bm_div(thisLayer.effect('Pseudo/ADBE Trace Path')('Pseudo/ADBE Trace Path-0001'), 100);\nvar pathToTrace = pathLayer('ADBE Root Vectors Group')(1)('ADBE Vectors Group')(1)('ADBE Vector Shape');\n$bm_rt = pathLayer.toComp(pathToTrace.pointOnPath(progress));","l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"Trace Path","np":4,"mn":"Pseudo/ADBE Trace Path","ix":1,"en":1,"ef":[{"ty":0,"nm":"Progress","mn":"Pseudo/ADBE Trace Path-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[0.202]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.511]},"t":55,"s":[34.982]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72.429,"s":[65.714]},{"t":96,"s":[100]}],"ix":1}},{"ty":7,"nm":"Loop","mn":"Pseudo/ADBE Trace Path-0002","ix":2,"v":{"a":0,"k":1,"ix":2}}]}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[750,325,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-61.099,-145.373],[13.542,2.851],[-122.588,27.083],[-53.454,81.963],[-210.242,-2.134],[-59.188,-77.906],[0,0]],"o":[[0,0],[20.669,49.178],[-13.542,-2.851],[122.588,-27.083],[53.454,-81.963],[140.406,1.425],[83.388,109.759],[0,0]],"v":[[-764.967,15.68],[-593.202,80.537],[-606.031,136.842],[-522.643,45.614],[-320.943,40.625],[14.035,-193.86],[274.178,-96.93],[450.932,-34.923]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":8,"ix":1}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Trace Shape Layer 2: Path 1 [1.1]').effect('Trace Path')('Progress');"},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"planes/1. Cover.ai","cl":"ai","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":29.49,"ix":10},"p":{"a":0,"k":[-16.71,-7.17,0],"ix":2,"l":2},"a":{"a":0,"k":[41.19,339.788,0],"ix":1,"l":2},"s":{"a":0,"k":[80,80,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[3.569,-6.866],[7.621,1.604],[-7.621,6.866]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[18.549,347.137],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-39.674,-9.397],[-31.072,10],[39.674,-9.999]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.19,322.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-29.373,0.483],[-16.669,27.038],[29.373,-27.038]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[51.491,339.788],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-35.374,-0.627],[-34.562,20.626],[-19.32,15.364],[35.373,-20.626]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[45.491,333.377],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":3,"nm":"Trace Shape Layer 1: Path 1 [1.1]","cl":"1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10,"x":"var $bm_rt;\nvar pathToTrace = thisComp.layer('Shape Layer 1')('ADBE Root Vectors Group')(1)('ADBE Vectors Group')(1)('ADBE Vector Shape');\nvar progress = $bm_div(thisLayer.effect('Pseudo/ADBE Trace Path')('Pseudo/ADBE Trace Path-0001'), 100);\nvar pathTan = pathToTrace.tangentOnPath(progress);\n$bm_rt = radiansToDegrees(Math.atan2(pathTan[1], pathTan[0]));"},"p":{"a":0,"k":[755,325,0],"ix":2,"x":"var $bm_rt;\nvar pathLayer = thisComp.layer('Shape Layer 1');\nvar progress = $bm_div(thisLayer.effect('Pseudo/ADBE Trace Path')('Pseudo/ADBE Trace Path-0001'), 100);\nvar pathToTrace = pathLayer('ADBE Root Vectors Group')(1)('ADBE Vectors Group')(1)('ADBE Vector Shape');\n$bm_rt = pathLayer.toComp(pathToTrace.pointOnPath(progress));","l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"Trace Path","np":4,"mn":"Pseudo/ADBE Trace Path","ix":1,"en":1,"ef":[{"ty":0,"nm":"Progress","mn":"Pseudo/ADBE Trace Path-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[0.202]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.511]},"t":55,"s":[32.312]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72.429,"s":[60.7]},{"t":96,"s":[100]}],"ix":1}},{"ty":7,"nm":"Loop","mn":"Pseudo/ADBE Trace Path-0002","ix":2,"v":{"a":0,"k":1,"ix":2}}]}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[750,325,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-61.099,-145.373],[13.542,2.851],[-122.588,27.083],[-53.454,81.963],[-210.252,-0.713],[-61.294,-76.261],[0,0]],"o":[[0,0],[20.669,49.178],[-13.542,-2.851],[122.588,-27.083],[53.454,-81.963],[210.252,0.713],[61.294,76.261],[0,0]],"v":[[-764.967,15.68],[-593.202,80.537],[-606.031,136.842],[-522.643,45.614],[-320.943,40.625],[14.035,-193.86],[324.781,28.509],[525.768,64.145]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":8,"ix":1}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Trace Shape Layer 1: Path 1 [1.1]').effect('Trace Path')('Progress');"},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":170,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/assets/stove.json b/app/src/main/assets/stove.json new file mode 100644 index 0000000..db1e616 --- /dev/null +++ b/app/src/main/assets/stove.json @@ -0,0 +1,8054 @@ +{ + "v": "5.7.14", + "fr": 29.9700012207031, + "ip": 0, + "op": 120.0000048877, + "w": 512, + "h": 512, + "nm": "gas stove composing", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "fire 1 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 30, + "s": [ + 256, + 208.978, + 0 + ], + "to": [ + 0, + -0.774, + 0 + ], + "ti": [ + 0, + 1.811, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 33, + "s": [ + 256, + 194.078, + 0 + ], + "to": [ + 0, + -0.919, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 36, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 43, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 46, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 47, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 54, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 57, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 58, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 65, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 68, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 69, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 76, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 79, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 80, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 87, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 90, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 91, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 98, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 101, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 102, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 109, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 112, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 113, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 120, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 123, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 124, + "s": [ + 256, + 201.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 131, + "s": [ + 256, + 196.978, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "t": 134.000005457932, + "s": [ + 256, + 201.978, + 0 + ] + } + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 14.351, + 23.165, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 30, + "s": [ + 71, + 71, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 33, + "s": [ + 113.05, + 113.05, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 36, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 119.000004846969, + "s": [ + 100, + 100, + 100 + ] + } + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 36, + "s": [ + { + "i": [ + [ + 0, + -7.788 + ], + [ + 7.789, + 0 + ], + [ + 0, + 7.788 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 7.788 + ], + [ + -7.788, + 0 + ], + [ + 0, + -7.788 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 14.102, + 8.813 + ], + [ + -0.001, + 22.915 + ], + [ + -14.102, + 8.813 + ], + [ + -0.001, + -22.915 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 50, + "s": [ + { + "i": [ + [ + 0, + -7.788 + ], + [ + 7.789, + 0 + ], + [ + 0, + 7.788 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 7.788 + ], + [ + -7.788, + 0 + ], + [ + 0, + -7.788 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 14.102, + 8.813 + ], + [ + -0.001, + 22.915 + ], + [ + -14.102, + 8.813 + ], + [ + -0.001, + -22.915 + ] + ], + "c": true + } + ] + }, + { + "t": 119.000004846969, + "s": [ + { + "i": [ + [ + 0, + -7.788 + ], + [ + 7.789, + 0 + ], + [ + 0, + 7.788 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 7.788 + ], + [ + -7.788, + 0 + ], + [ + 0, + -7.788 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 14.102, + 8.813 + ], + [ + -0.001, + 22.915 + ], + [ + -14.102, + 8.813 + ], + [ + -0.001, + -22.915 + ] + ], + "c": true + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 14.351, + 23.166 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 30.0000012219251, + "op": 150.000006109625, + "st": 30.0000012219251, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Layer 11 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 30, + "s": [ + 256, + 223.266, + 0 + ], + "to": [ + 0, + -0.774, + 0 + ], + "ti": [ + 0, + 1.811, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 33, + "s": [ + 256, + 218.366, + 0 + ], + "to": [ + 0, + -0.919, + 0 + ], + "ti": [ + 0, + 0.393, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 36, + "s": [ + 256, + 216.266, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 50, + "s": [ + 256, + 216.266, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "t": 119.000004846969, + "s": [ + 256, + 216.266, + 0 + ] + } + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 27.174, + 10.516, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 30, + "s": [ + 71, + 71, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 33, + "s": [ + 113.05, + 113.05, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 36, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 119.000004846969, + "s": [ + 100, + 100, + 100 + ] + } + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 36, + "s": [ + { + "i": [ + [ + -6.904, + 6.866 + ], + [ + 10.522, + 0 + ], + [ + 6.881, + -5.818 + ], + [ + -10.506, + 0 + ] + ], + "o": [ + [ + -6.882, + -5.818 + ], + [ + -10.521, + 0 + ], + [ + 6.904, + 6.866 + ], + [ + 10.506, + 0 + ] + ], + "v": [ + [ + 26.924, + -0.846 + ], + [ + -0.001, + -10.266 + ], + [ + -26.924, + -0.846 + ], + [ + -0.001, + 10.266 + ] + ], + "c": true + } + ] + }, + { + "t": 50.0000020365418, + "s": [ + { + "i": [ + [ + -6.904, + 6.866 + ], + [ + 10.522, + 0 + ], + [ + 6.881, + -5.818 + ], + [ + -10.506, + 0 + ] + ], + "o": [ + [ + -6.882, + -5.818 + ], + [ + -10.521, + 0 + ], + [ + 6.904, + 6.866 + ], + [ + 10.506, + 0 + ] + ], + "v": [ + [ + 26.924, + -0.846 + ], + [ + -0.001, + -10.266 + ], + [ + -26.924, + -0.846 + ], + [ + -0.001, + 10.266 + ] + ], + "c": true + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 1, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 27.174, + 10.516 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 30.0000012219251, + "op": 150.000006109625, + "st": 30.0000012219251, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "fire 2 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 30, + "s": [ + 256, + 189.513, + 0 + ], + "to": [ + 0, + -2.433, + 0 + ], + "ti": [ + 0, + 5.692, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 33, + "s": [ + 256, + 164.114, + 0 + ], + "to": [ + 0, + -2.887, + 0 + ], + "ti": [ + 0, + 1.234, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 36, + "s": [ + 256, + 167.513, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 50, + "s": [ + 256, + 167.513, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "t": 119.000004846969, + "s": [ + 256, + 167.513, + 0 + ] + } + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 38.437, + 48.157, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 30, + "s": [ + 71, + 71, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 33, + "s": [ + 113.05, + 113.05, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 36, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 50, + "s": [ + 100, + 100, + 100 + ] + }, + { + "t": 119.000004846969, + "s": [ + 100, + 100, + 100 + ] + } + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 36, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 43, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 47, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 48, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 55, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 59, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 60, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 67, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 71, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 72, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 79, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 83, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 84, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 91, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 95, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 96, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 103, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 107, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 108, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 115, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 119, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 120, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 127, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + 0, + -52.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + }, + { + "t": 131.000005335739, + "s": [ + { + "i": [ + [ + -6.882, + -5.818 + ], + [ + 0, + 10.584 + ], + [ + 4.732, + 0 + ], + [ + 0, + -21.09 + ], + [ + -6.955, + -6.916 + ], + [ + -10.521, + 0 + ] + ], + "o": [ + [ + 6.954, + -6.916 + ], + [ + 0, + -21.09 + ], + [ + -4.731, + 0 + ], + [ + 0, + 10.584 + ], + [ + 6.881, + -5.818 + ], + [ + 10.522, + 0 + ] + ], + "v": [ + [ + 26.925, + 47.907 + ], + [ + 38.187, + 20.831 + ], + [ + -0.001, + -47.907 + ], + [ + -38.187, + 20.831 + ], + [ + -26.924, + 47.907 + ], + [ + -0.001, + 38.487 + ] + ], + "c": true + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 1, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 38.437, + 48.157 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 30.0000012219251, + "op": 150.000006109625, + "st": 30.0000012219251, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "fire 3 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 30, + "s": [ + 256, + 190.069, + 0 + ], + "to": [ + 0, + -2.433, + 0 + ], + "ti": [ + 0, + 5.692, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 33, + "s": [ + 256, + 164.669, + 0 + ], + "to": [ + 0, + -2.887, + 0 + ], + "ti": [ + 0, + 1.234, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 36, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 43, + "s": [ + 256, + 166.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 50, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 1.234, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 51, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 58, + "s": [ + 256, + 166.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 65, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 1.234, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 66, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 73, + "s": [ + 256, + 166.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 80, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 1.234, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 81, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 88, + "s": [ + 256, + 166.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 95, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 1.234, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 96, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 103, + "s": [ + 256, + 166.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 110, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 1.234, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 111, + "s": [ + 256, + 168.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 118, + "s": [ + 256, + 166.069, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "t": 125.000005091354, + "s": [ + 256, + 168.069, + 0 + ] + } + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 40.792, + 62.319, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 30, + "s": [ + 71, + 71, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 33, + "s": [ + 113.05, + 113.05, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 36, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 43, + "s": [ + 105, + 105, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 50, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 51, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 58, + "s": [ + 105, + 105, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 65, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 66, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 73, + "s": [ + 105, + 105, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 80, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 81, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 88, + "s": [ + 105, + 105, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 95, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 96, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 103, + "s": [ + 105, + 105, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 110, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 111, + "s": [ + 100, + 100, + 100 + ] + }, + { + "i": { + "x": [ + 0.833, + 0.833, + 0.833 + ], + "y": [ + 0.833, + 0.833, + 0.833 + ] + }, + "o": { + "x": [ + 0.167, + 0.167, + 0.167 + ], + "y": [ + 0.167, + 0.167, + 0.167 + ] + }, + "t": 118, + "s": [ + 105, + 105, + 100 + ] + }, + { + "t": 125.000005091354, + "s": [ + 100, + 100, + 100 + ] + } + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + -22.391 + ], + [ + 22.391, + 0 + ], + [ + 0, + 22.391 + ], + [ + -5.023, + 0 + ] + ], + "o": [ + [ + 0, + 22.391 + ], + [ + -22.391, + 0 + ], + [ + 0, + -22.391 + ], + [ + 5.022, + 0 + ] + ], + "v": [ + [ + 40.542, + 21.526 + ], + [ + 0, + 62.069 + ], + [ + -40.542, + 21.526 + ], + [ + 0, + -62.069 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.463000009574, + 0.224000010771, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 40.792, + 62.319 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 30.0000012219251, + "op": 150.000006109625, + "st": 30.0000012219251, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "knob 1 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 20, + "s": [ + 0 + ] + }, + { + "t": 30.0000012219251, + "s": [ + 34 + ] + } + ], + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 255.5, + 346.5, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 11.25, + 40.25, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ], + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ] + ], + "v": [ + [ + 0, + 40 + ], + [ + 0, + 40 + ], + [ + -11, + 29 + ], + [ + -11, + -29 + ], + [ + 0, + -40 + ], + [ + 11, + -29 + ], + [ + 11, + 29 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0, + 0, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 11.25, + 40.25 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 120.0000048877, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "knob 2 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 256, + 346.5, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 22.25, + 22.25, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + -12.15 + ], + [ + 12.15, + 0 + ], + [ + 0, + 12.15 + ], + [ + -12.15, + 0 + ] + ], + "o": [ + [ + 0, + 12.15 + ], + [ + -12.15, + 0 + ], + [ + 0, + -12.15 + ], + [ + 12.15, + 0 + ] + ], + "v": [ + [ + 22, + 0 + ], + [ + 0, + 22 + ], + [ + -22, + 0 + ], + [ + 0, + -22 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.757000014361, + 0.152999997606, + 0.176000004189, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 22.25, + 22.25 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 120.0000048877, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "knob 3 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 256, + 346.5, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 49.25, + 49.25, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + -27.062 + ], + [ + 27.062, + 0 + ], + [ + 0, + 27.062 + ], + [ + -27.062, + 0 + ] + ], + "o": [ + [ + 0, + 27.062 + ], + [ + -27.062, + 0 + ], + [ + 0, + -27.062 + ], + [ + 27.062, + 0 + ] + ], + "v": [ + [ + 49, + 0 + ], + [ + 0, + 49 + ], + [ + -49, + 0 + ], + [ + 0, + -49 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.2, + 0.2, + 0.2, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 49.25, + 49.25 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 120.0000048877, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 4, + "nm": "body effect Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 261.707, + 336.901, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 184.868, + 66.151, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 17.306, + 0 + ], + [ + 0, + 0 + ], + [ + 4.121, + -14.621 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + -2.215, + -17.163 + ], + [ + 0, + 0 + ], + [ + -15.594, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 171.484, + -35.888 + ], + [ + 137.35, + -65.901 + ], + [ + -151.5, + -65.901 + ], + [ + -184.618, + -40.845 + ], + [ + 184.618, + 65.901 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.654999976065, + 0.855000035903, + 0.976000019148, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 184.868, + 66.151 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 120.0000048877, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 9, + "ty": 4, + "nm": "body Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 256, + 346, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 197.001, + 77.75, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 11.015, + 0 + ], + [ + 0, + 0 + ], + [ + -1.41, + 10.924 + ], + [ + 0, + 0 + ], + [ + -17.633, + 0 + ], + [ + 0, + 0 + ], + [ + -2.257, + -17.489 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -11.014, + 0 + ], + [ + 0, + 0 + ], + [ + 2.256, + -17.489 + ], + [ + 0, + 0 + ], + [ + 17.632, + 0 + ], + [ + 0, + 0 + ], + [ + 1.41, + 10.924 + ] + ], + "v": [ + [ + 177.229, + 77.5 + ], + [ + -177.23, + 77.5 + ], + [ + -195.342, + 56.9 + ], + [ + -181.945, + -46.918 + ], + [ + -147.165, + -77.5 + ], + [ + 147.165, + -77.5 + ], + [ + 181.946, + -46.918 + ], + [ + 195.341, + 56.9 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.556999954523, + 0.717999985639, + 0.838999968884, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 197.001, + 77.75 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 120.0000048877, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 10, + "ty": 4, + "nm": "pan support effect Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 255.886, + 255.5, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 79.317, + 14.75, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + -4.33, + 7.172 + ], + [ + 1.841, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + -1.524, + -0.799 + ], + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 11.732, + -4.786 + ] + ], + "v": [ + [ + 12.405, + -8.507 + ], + [ + 7.308, + -9.764 + ], + [ + -1.405, + -9.764 + ], + [ + -12.405, + 1.237 + ], + [ + -12.405, + 9.764 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.463000009574, + 0.224000010771, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 145.979, + 10.014 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 1.328, + -0.569 + ], + [ + -11.437, + -4.818 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -1.535, + 0 + ], + [ + 4.131, + 7.122 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ] + ], + "v": [ + [ + 1.019, + -9.562 + ], + [ + -7.695, + -9.562 + ], + [ + -12.019, + -8.677 + ], + [ + 12.019, + 9.562 + ], + [ + 12.019, + 1.438 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.463000009574, + 0.224000010771, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 12.269, + 9.812 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ], + [ + -4.776, + 0 + ], + [ + -4.235, + 0.312 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 4.552, + 0.363 + ], + [ + 4.425, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ] + ], + "v": [ + [ + 2.5, + -11.75 + ], + [ + -2.5, + -11.75 + ], + [ + -13.5, + -0.75 + ], + [ + -13.5, + 11.187 + ], + [ + 0.5, + 11.75 + ], + [ + 13.5, + 11.27 + ], + [ + 13.5, + -0.75 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.463000009574, + 0.224000010771, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 78.931, + 17.5 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 3", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 3, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 30.0000012219251, + "op": 150.000006109625, + "st": 30.0000012219251, + "bm": 0 + }, + { + "ddd": 0, + "ind": 11, + "ty": 4, + "nm": "pan support Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 255.5, + 286.5, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 85.606, + 45.75, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ], + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ], + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ] + ], + "v": [ + [ + 4.357, + 45.5 + ], + [ + -4.357, + 45.5 + ], + [ + -15.357, + 34.5 + ], + [ + -15.357, + -34.5 + ], + [ + -4.357, + -45.5 + ], + [ + 4.357, + -45.5 + ], + [ + 15.357, + -34.5 + ], + [ + 15.357, + 34.5 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.301999978458, + 0.301999978458, + 0.301999978458, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 155.607, + 45.75 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ], + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ], + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ] + ], + "v": [ + [ + 4.357, + 45.5 + ], + [ + -4.357, + 45.5 + ], + [ + -15.357, + 34.5 + ], + [ + -15.357, + -34.5 + ], + [ + -4.357, + -45.5 + ], + [ + 4.357, + -45.5 + ], + [ + 15.357, + -34.5 + ], + [ + 15.357, + 34.5 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.301999978458, + 0.301999978458, + 0.301999978458, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 15.606, + 45.75 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ], + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -6.075 + ], + [ + 0, + 0 + ], + [ + 6.075, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.075 + ] + ], + "v": [ + [ + 2.5, + 40 + ], + [ + -2.5, + 40 + ], + [ + -13.5, + 29 + ], + [ + -13.5, + -29 + ], + [ + -2.5, + -40 + ], + [ + 2.5, + -40 + ], + [ + 13.5, + -29 + ], + [ + 13.5, + 29 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.301999978458, + 0.301999978458, + 0.301999978458, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 85.607, + 45.75 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 3", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 3, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 120.0000048877, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..76c950b --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.22.1) +project(SerialPortNativeLib C CXX) # Added for good practice + +add_library(serial_port SHARED + SerialPort.c) + +# Compatibility with 16K pages for Android 15+ support on 64-bit devices. +# See https://developer.android.com/guide/practices/page-sizes +if(ANDROID_ABI STREQUAL "arm64-v8a") + target_link_options(serial_port PRIVATE "-Wl,-z,max-page-size=16384") +endif() \ No newline at end of file diff --git a/app/src/main/cpp/SerialPort.c b/app/src/main/cpp/SerialPort.c new file mode 100644 index 0000000..d20cbe0 --- /dev/null +++ b/app/src/main/cpp/SerialPort.c @@ -0,0 +1,149 @@ +#include "SerialPort.h" + +#include +#include +#include + +#define MAX_THREAD_LIMIT 32 + + +static speed_t getBaudrate(jint baudrate) +{ + switch(baudrate) { + case 0: return B0; + case 50: return B50; + case 75: return B75; + case 110: return B110; + case 134: return B134; + case 150: return B150; + case 200: return B200; + case 300: return B300; + case 600: return B600; + case 1200: return B1200; + case 1800: return B1800; + case 2400: return B2400; + case 4800: return B4800; + case 9600: return B9600; + case 19200: return B19200; + case 38400: return B38400; + case 57600: return B57600; + case 115200: return B115200; + case 230400: return B230400; + case 460800: return B460800; + case 500000: return B500000; + case 576000: return B576000; + case 921600: return B921600; + case 1000000: return B1000000; + case 1152000: return B1152000; + case 1500000: return B1500000; + case 2000000: return B2000000; + case 2500000: return B2500000; + case 3000000: return B3000000; + case 3500000: return B3500000; + case 4000000: return B4000000; + default: return -1; + } +} + +struct { + pthread_t tid; + jobject clazz; +} threads[MAX_THREAD_LIMIT]; +JavaVM *vm; +jfieldID mFD; +jmethodID onNativeData; + +void *comm_read() { + JNIEnv *env; + (*vm)->AttachCurrentThread(vm, &env, NULL); + jclass clazz = NULL; + for (int i = 0; i < MAX_THREAD_LIMIT; i++) { + if (pthread_equal(pthread_self(), threads[i].tid)) { + clazz = threads[i].clazz; + if (mFD == NULL || onNativeData == NULL) { + jclass obj = (*env)->GetObjectClass(env, clazz); + mFD = (*env)->GetFieldID(env, obj, "mFD", "I"); + onNativeData = (*env)->GetMethodID(env, obj, "onNativeData", "([B)V"); + } + break; + } + } + if (clazz != NULL) { + int fd; + while ((fd = (*env)->GetIntField(env, clazz, mFD)) != -1) { + int size = 1024; + jbyte data[size]; + int len = read(fd, data, size); + if (len > 0) { + jbyteArray bytes = (*env)->NewByteArray(env, len); + (*env)->SetByteArrayRegion(env, bytes, 0, len, data); + (*env)->CallVoidMethod(env, clazz, onNativeData, bytes); + } else if (len < 0) { + // DETECT: Hardware level error (e.g. unplugged) + // Send a specific error byte like 0x0E to trigger the Error result in Repository + jbyteArray errorBytes = (*env)->NewByteArray(env, 1); + jbyte err = 0x0E; + (*env)->SetByteArrayRegion(env, errorBytes, 0, 1, &err); + (*env)->CallVoidMethod(env, clazz, onNativeData, errorBytes); + } + usleep(10000); + } + } + if (vm != NULL) { + int isFree = 1; + for (int i = 0; i < MAX_THREAD_LIMIT; i++) { + if (threads[i].clazz != NULL) { + if (threads[i].clazz == clazz) { + (*env)->DeleteGlobalRef(env, clazz); + threads[i].clazz = clazz = NULL; + } else { + isFree = 0; + if (clazz == NULL) break; + } + } + } + (*vm)->DetachCurrentThread(vm); + if (isFree) { + onNativeData = NULL; + mFD = NULL; + vm = NULL; + } + } + return 0; +} + +JNIEXPORT void JNICALL +Java_com_laseroptek_raman_data_source_serial_SerialPort_open(JNIEnv *env, jclass clazz, int fd, int baudrate) { + tcflush(fd, TCIOFLUSH); + struct termios cfg; + tcgetattr(fd, &cfg); + cfmakeraw(&cfg); + speed_t speed = getBaudrate(baudrate); + cfsetispeed(&cfg, speed); + cfsetospeed(&cfg, speed); + tcsetattr(fd, TCSANOW, &cfg); + for (int i = 0; i < MAX_THREAD_LIMIT; i++) { + if (threads[i].clazz == NULL) { + if (vm == NULL) { + (*env)->GetJavaVM(env, &vm); + } + threads[i].clazz = (*env)->NewGlobalRef(env, clazz); + pthread_create(&threads[i].tid, NULL, comm_read, NULL); + break; + } + } +} + +JNIEXPORT void JNICALL +Java_com_laseroptek_raman_data_source_serial_SerialPort_write(JNIEnv *env, __unused jclass _, int fd, jbyteArray bytes) { + jbyte *data = (*env)->GetByteArrayElements(env, bytes, NULL); + if (data != NULL) { + write(fd, data, (*env)->GetArrayLength(env, bytes)); + (*env)->ReleaseByteArrayElements(env, bytes, data, JNI_ABORT); + } +} + +JNIEXPORT void JNICALL +Java_com_laseroptek_raman_data_source_serial_SerialPort_close(__unused JNIEnv *_, __unused jclass __, int fd) { + close(fd); +} diff --git a/app/src/main/cpp/SerialPort.h b/app/src/main/cpp/SerialPort.h new file mode 100644 index 0000000..e55db91 --- /dev/null +++ b/app/src/main/cpp/SerialPort.h @@ -0,0 +1,21 @@ +#include + +#ifndef _SERIAL_PORT +#define _SERIAL_PORT +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL +Java_com_laseroptek_raman_data_source_serial_SerialPort_open(JNIEnv *, jobject, int, int); + +JNIEXPORT void JNICALL +Java_com_laseroptek_raman_data_source_serial_SerialPort_write(JNIEnv *, jobject, int, jbyteArray); + +JNIEXPORT void JNICALL +Java_com_laseroptek_raman_data_source_serial_SerialPort_close(JNIEnv *, jobject, int); + +#ifdef __cplusplus +} +#endif +#endif \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/VasCURA589App.kt b/app/src/main/java/com/laseroptek/raman/VasCURA589App.kt new file mode 100644 index 0000000..2169911 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/VasCURA589App.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * 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. + */ + +package com.laseroptek.raman + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber + +@HiltAndroidApp +class VasCURA589App : Application() { + override fun onCreate() { + super.onCreate() + + if (BuildConfig.DEBUG) { + Timber.plant(object : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + super.log(priority, "$tag", message, t) + } + + override fun createStackElementTag(element: StackTraceElement): String { + return "[%s:%s:%s]".format(element.fileName, element.lineNumber, element.methodName) + } + }) + } + } +} diff --git a/app/src/main/java/com/laseroptek/raman/const/EnegryTable_5_5.kt b/app/src/main/java/com/laseroptek/raman/const/EnegryTable_5_5.kt new file mode 100644 index 0000000..5bdebee --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/EnegryTable_5_5.kt @@ -0,0 +1,653 @@ +package com.laseroptek.raman.const + +// +// Energy Table1 (HANDPIENCE-5x5) +// - Map, Float> +// - Map, VALUE> +// - Map, Energy(J)> +// + +val EnergyTable_5_5 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f,2.0f) to 0.500f, + Pair(0.5f,2.1f) to 0.525f, + Pair(0.5f,2.2f) to 0.550f, + Pair(0.5f,2.3f) to 0.575f, + Pair(0.5f,2.4f) to 0.600f, + Pair(0.5f,2.5f) to 0.625f, + Pair(0.5f,2.6f) to 0.650f, + Pair(0.5f,2.7f) to 0.675f, + Pair(0.5f,2.8f) to 0.700f, + Pair(0.5f,2.9f) to 0.725f, + Pair(0.5f,3.0f) to 0.750f, + Pair(0.5f,3.2f) to 0.800f, + Pair(0.5f,3.4f) to 0.850f, + Pair(0.5f,3.6f) to 0.900f, + Pair(0.5f,3.8f) to 0.950f, + Pair(0.5f,4.0f) to 1.000f, + Pair(0.5f,4.2f) to 1.050f, + Pair(0.5f,4.4f) to 1.100f, + Pair(0.5f,4.6f) to 1.150f, + Pair(0.5f,4.8f) to 1.200f, + Pair(0.5f,5.0f) to 1.250f, + Pair(0.5f,5.5f) to 1.375f, + Pair(0.5f,6.0f) to 1.500f, + Pair(0.5f,6.5f) to 1.625f, + Pair(0.5f,7.0f) to 1.750f, + Pair(0.5f,7.5f) to 1.875f, + Pair(0.5f,8.0f) to 2.000f, + + // Pulse Width = 1f + Pair(1f, 2.0f) to 0.500f, + Pair(1f, 2.1f) to 0.525f, + Pair(1f, 2.2f) to 0.550f, + Pair(1f, 2.3f) to 0.575f, + Pair(1f, 2.4f) to 0.600f, + Pair(1f, 2.5f) to 0.625f, + Pair(1f, 2.6f) to 0.650f, + Pair(1f, 2.7f) to 0.675f, + Pair(1f, 2.8f) to 0.700f, + Pair(1f, 2.9f) to 0.725f, + Pair(1f, 3.0f) to 0.750f, + Pair(1f, 3.2f) to 0.800f, + Pair(1f, 3.4f) to 0.850f, + Pair(1f, 3.6f) to 0.900f, + Pair(1f, 3.8f) to 0.950f, + Pair(1f, 4.0f) to 1.000f, + Pair(1f, 4.2f) to 1.050f, + Pair(1f, 4.4f) to 1.100f, + Pair(1f, 4.6f) to 1.150f, + Pair(1f, 4.8f) to 1.200f, + Pair(1f, 5.0f) to 1.250f, + Pair(1f, 5.5f) to 1.375f, + Pair(1f, 6.0f) to 1.500f, + Pair(1f, 6.5f) to 1.625f, + Pair(1f, 7.0f) to 1.750f, + Pair(1f, 7.5f) to 1.875f, + Pair(1f, 8.0f) to 2.000f, + Pair(1f, 8.5f) to 2.125f, + Pair(1f, 9.0f) to 2.250f, + Pair(1f, 9.5f) to 2.375f, + Pair(1f, 10.0f) to 2.500f, + Pair(1f, 11.0f) to 2.750f, + Pair(1f, 12.0f) to 3.000f, + + // Pulse Width = 1.5f + Pair(1.5f, 2.0f) to 0.500f, + Pair(1.5f, 2.1f) to 0.525f, + Pair(1.5f, 2.2f) to 0.550f, + Pair(1.5f, 2.3f) to 0.575f, + Pair(1.5f, 2.4f) to 0.600f, + Pair(1.5f, 2.5f) to 0.625f, + Pair(1.5f, 2.6f) to 0.650f, + Pair(1.5f, 2.7f) to 0.675f, + Pair(1.5f, 2.8f) to 0.700f, + Pair(1.5f, 2.9f) to 0.725f, + Pair(1.5f, 3.0f) to 0.750f, + Pair(1.5f, 3.2f) to 0.800f, + Pair(1.5f, 3.4f) to 0.850f, + Pair(1.5f, 3.6f) to 0.900f, + Pair(1.5f, 3.8f) to 0.950f, + Pair(1.5f, 4.0f) to 1.000f, + Pair(1.5f, 4.2f) to 1.050f, + Pair(1.5f, 4.4f) to 1.100f, + Pair(1.5f, 4.6f) to 1.150f, + Pair(1.5f, 4.8f) to 1.200f, + Pair(1.5f, 5.0f) to 1.250f, + Pair(1.5f, 5.5f) to 1.375f, + Pair(1.5f, 6.0f) to 1.500f, + Pair(1.5f, 6.5f) to 1.625f, + Pair(1.5f, 7.0f) to 1.750f, + Pair(1.5f, 7.5f) to 1.875f, + Pair(1.5f, 8.0f) to 2.000f, + Pair(1.5f, 8.5f) to 2.125f, + Pair(1.5f, 9.0f) to 2.250f, + Pair(1.5f, 9.5f) to 2.375f, + Pair(1.5f, 10.0f) to 2.500f, + Pair(1.5f, 11.0f) to 2.750f, + Pair(1.5f, 12.0f) to 3.000f, + Pair(1.5f, 13.0f) to 3.250f, + Pair(1.5f, 14.0f) to 3.500f, + Pair(1.5f, 15.0f) to 3.750f, + Pair(1.5f, 16.0f) to 4.000f, + + // Pulse Width = 3f + Pair(3f, 2.0f ) to 0.500f, + Pair(3f, 2.1f ) to 0.525f, + Pair(3f, 2.2f ) to 0.550f, + Pair(3f, 2.3f ) to 0.575f, + Pair(3f, 2.4f ) to 0.600f, + Pair(3f, 2.5f ) to 0.625f, + Pair(3f, 2.6f ) to 0.650f, + Pair(3f, 2.7f ) to 0.675f, + Pair(3f, 2.8f ) to 0.700f, + Pair(3f, 2.9f ) to 0.725f, + Pair(3f, 3.0f ) to 0.750f, + Pair(3f, 3.2f ) to 0.800f, + Pair(3f, 3.4f ) to 0.850f, + Pair(3f, 3.6f ) to 0.900f, + Pair(3f, 3.8f ) to 0.950f, + Pair(3f, 4.0f ) to 1.000f, + Pair(3f, 4.2f ) to 1.050f, + Pair(3f, 4.4f ) to 1.100f, + Pair(3f, 4.6f ) to 1.150f, + Pair(3f, 4.8f ) to 1.200f, + Pair(3f, 5.0f ) to 1.250f, + Pair(3f, 5.5f ) to 1.375f, + Pair(3f, 6.0f ) to 1.500f, + Pair(3f, 6.5f ) to 1.625f, + Pair(3f, 7.0f ) to 1.750f, + Pair(3f, 7.5f ) to 1.875f, + Pair(3f, 8.0f ) to 2.000f, + Pair(3f, 8.5f ) to 2.125f, + Pair(3f, 9.0f ) to 2.250f, + Pair(3f, 9.5f ) to 2.375f, + Pair(3f, 10.0f) to 2.500f, + Pair(3f, 11.0f) to 2.750f, + Pair(3f, 12.0f) to 3.000f, + Pair(3f, 13.0f) to 3.250f, + Pair(3f, 14.0f) to 3.500f, + Pair(3f, 15.0f) to 3.750f, + Pair(3f, 16.0f) to 4.000f, + Pair(3f, 17.0f) to 4.250f, + Pair(3f, 18.0f) to 4.500f, + Pair(3f, 19.0f) to 4.750f, + Pair(3f, 20.0f) to 5.000f, + Pair(3f, 21.0f) to 5.250f, + Pair(3f, 22.0f) to 5.500f, + Pair(3f, 23.0f) to 5.750f, + Pair(3f, 24.0f) to 6.000f, + + // Pulse Width = 5f + Pair(5f, 2.0f) to 0.500f, + Pair(5f, 2.1f) to 0.525f, + Pair(5f, 2.2f) to 0.550f, + Pair(5f, 2.3f) to 0.575f, + Pair(5f, 2.4f) to 0.600f, + Pair(5f, 2.5f) to 0.625f, + Pair(5f, 2.6f) to 0.650f, + Pair(5f, 2.7f) to 0.675f, + Pair(5f, 2.8f) to 0.700f, + Pair(5f, 2.9f) to 0.725f, + Pair(5f, 3.0f) to 0.750f, + Pair(5f, 3.2f) to 0.800f, + Pair(5f, 3.4f) to 0.850f, + Pair(5f, 3.6f) to 0.900f, + Pair(5f, 3.8f) to 0.950f, + Pair(5f, 4.0f) to 1.000f, + Pair(5f, 4.2f) to 1.050f, + Pair(5f, 4.4f) to 1.100f, + Pair(5f, 4.6f) to 1.150f, + Pair(5f, 4.8f) to 1.200f, + Pair(5f, 5.0f) to 1.250f, + Pair(5f, 5.5f) to 1.375f, + Pair(5f, 6.0f) to 1.500f, + Pair(5f, 6.5f) to 1.625f, + Pair(5f, 7.0f) to 1.750f, + Pair(5f, 7.5f) to 1.875f, + Pair(5f, 8.0f) to 2.000f, + Pair(5f, 8.5f) to 2.125f, + Pair(5f, 9.0f) to 2.250f, + Pair(5f, 9.5f) to 2.375f, + Pair(5f, 10.0f) to 2.500f, + Pair(5f, 11.0f) to 2.750f, + Pair(5f, 12.0f) to 3.000f, + Pair(5f, 13.0f) to 3.250f, + Pair(5f, 14.0f) to 3.500f, + Pair(5f, 15.0f) to 3.750f, + Pair(5f, 16.0f) to 4.000f, + Pair(5f, 17.0f) to 4.250f, + Pair(5f, 18.0f) to 4.500f, + Pair(5f, 19.0f) to 4.750f, + Pair(5f, 20.0f) to 5.000f, + Pair(5f, 21.0f) to 5.250f, + Pair(5f, 22.0f) to 5.500f, + Pair(5f, 23.0f) to 5.750f, + Pair(5f, 24.0f) to 6.000f, + Pair(5f, 25.0f) to 6.250f, + Pair(5f, 26.0f) to 6.500f, + Pair(5f, 27.0f) to 6.750f, + Pair(5f, 28.0f) to 7.000f, + + // Pulse Width = 10f + Pair(10f, 2.0f) to 0.500f, + Pair(10f, 2.1f) to 0.525f, + Pair(10f, 2.2f) to 0.550f, + Pair(10f, 2.3f) to 0.575f, + Pair(10f, 2.4f) to 0.600f, + Pair(10f, 2.5f) to 0.625f, + Pair(10f, 2.6f) to 0.650f, + Pair(10f, 2.7f) to 0.675f, + Pair(10f, 2.8f) to 0.700f, + Pair(10f, 2.9f) to 0.725f, + Pair(10f, 3.0f) to 0.750f, + Pair(10f, 3.2f) to 0.800f, + Pair(10f, 3.4f) to 0.850f, + Pair(10f, 3.6f) to 0.900f, + Pair(10f, 3.8f) to 0.950f, + Pair(10f, 4.0f) to 1.000f, + Pair(10f, 4.2f) to 1.050f, + Pair(10f, 4.4f) to 1.100f, + Pair(10f, 4.6f) to 1.150f, + Pair(10f, 4.8f) to 1.200f, + Pair(10f, 5.0f) to 1.250f, + Pair(10f, 5.5f) to 1.375f, + Pair(10f, 6.0f) to 1.500f, + Pair(10f, 6.5f) to 1.625f, + Pair(10f, 7.0f) to 1.750f, + Pair(10f, 7.5f) to 1.875f, + Pair(10f, 8.0f) to 2.000f, + Pair(10f, 8.5f) to 2.125f, + Pair(10f, 9.0f) to 2.250f, + Pair(10f, 9.5f) to 2.375f, + Pair(10f, 10.0f) to 2.500f, + Pair(10f, 11.0f) to 2.750f, + Pair(10f, 12.0f) to 3.000f, + Pair(10f, 13.0f) to 3.250f, + Pair(10f, 14.0f) to 3.500f, + Pair(10f, 15.0f) to 3.750f, + Pair(10f, 16.0f) to 4.000f, + Pair(10f, 17.0f) to 4.250f, + Pair(10f, 18.0f) to 4.500f, + Pair(10f, 19.0f) to 4.750f, + Pair(10f, 20.0f) to 5.000f, + Pair(10f, 21.0f) to 5.250f, + Pair(10f, 22.0f) to 5.500f, + Pair(10f, 23.0f) to 5.750f, + Pair(10f, 24.0f) to 6.000f, + Pair(10f, 25.0f) to 6.250f, + Pair(10f, 26.0f) to 6.500f, + Pair(10f, 27.0f) to 6.750f, + Pair(10f, 28.0f) to 7.000f, + Pair(10f, 29.0f) to 7.250f, + Pair(10f, 30.0f) to 7.500f, + Pair(10f, 31.0f) to 7.750f, + Pair(10f, 32.0f) to 8.000f, + Pair(10f, 33.0f) to 8.250f, + Pair(10f, 34.0f) to 8.500f, + Pair(10f, 35.0f) to 8.750f, + Pair(10f, 36.0f) to 9.000f, + Pair(10f, 37.0f) to 9.250f, + Pair(10f, 38.0f) to 9.500f, + Pair(10f, 39.0f) to 9.750f, + Pair(10f, 40.0f) to 10.000f, + + // Pulse Width = 15f + Pair(15f, 2.0f) to 0.500f, + Pair(15f, 2.1f) to 0.525f, + Pair(15f, 2.2f) to 0.550f, + Pair(15f, 2.3f) to 0.575f, + Pair(15f, 2.4f) to 0.600f, + Pair(15f, 2.5f) to 0.625f, + Pair(15f, 2.6f) to 0.650f, + Pair(15f, 2.7f) to 0.675f, + Pair(15f, 2.8f) to 0.700f, + Pair(15f, 2.9f) to 0.725f, + Pair(15f, 3.0f) to 0.750f, + Pair(15f, 3.2f) to 0.800f, + Pair(15f, 3.4f) to 0.850f, + Pair(15f, 3.6f) to 0.900f, + Pair(15f, 3.8f) to 0.950f, + Pair(15f, 4.0f) to 1.000f, + Pair(15f, 4.2f) to 1.050f, + Pair(15f, 4.4f) to 1.100f, + Pair(15f, 4.6f) to 1.150f, + Pair(15f, 4.8f) to 1.200f, + Pair(15f, 5.0f) to 1.250f, + Pair(15f, 5.5f) to 1.375f, + Pair(15f, 6.0f) to 1.500f, + Pair(15f, 6.5f) to 1.625f, + Pair(15f, 7.0f) to 1.750f, + Pair(15f, 7.5f) to 1.875f, + Pair(15f, 8.0f) to 2.000f, + Pair(15f, 8.5f) to 2.125f, + Pair(15f, 9.0f) to 2.250f, + Pair(15f, 9.5f) to 2.375f, + Pair(15f, 10.0f) to 2.500f, + Pair(15f, 11.0f) to 2.750f, + Pair(15f, 12.0f) to 3.000f, + Pair(15f, 13.0f) to 3.250f, + Pair(15f, 14.0f) to 3.500f, + Pair(15f, 15.0f) to 3.750f, + Pair(15f, 16.0f) to 4.000f, + Pair(15f, 17.0f) to 4.250f, + Pair(15f, 18.0f) to 4.500f, + Pair(15f, 19.0f) to 4.750f, + Pair(15f, 20.0f) to 5.000f, + Pair(15f, 21.0f) to 5.250f, + Pair(15f, 22.0f) to 5.500f, + Pair(15f, 23.0f) to 5.750f, + Pair(15f, 24.0f) to 6.000f, + Pair(15f, 25.0f) to 6.250f, + Pair(15f, 26.0f) to 6.500f, + Pair(15f, 27.0f) to 6.750f, + Pair(15f, 28.0f) to 7.000f, + Pair(15f, 29.0f) to 7.250f, + Pair(15f, 30.0f) to 7.500f, + Pair(15f, 31.0f) to 7.750f, + Pair(15f, 32.0f) to 8.000f, + Pair(15f, 33.0f) to 8.250f, + Pair(15f, 34.0f) to 8.500f, + Pair(15f, 35.0f) to 8.750f, + Pair(15f, 36.0f) to 9.000f, + Pair(15f, 37.0f) to 9.250f, + Pair(15f, 38.0f) to 9.500f, + Pair(15f, 39.0f) to 9.750f, + Pair(15f, 40.0f) to 10.000f, + + // Pulse Width = 20f + Pair(20f, 2.0f) to 0.500f, + Pair(20f, 2.1f) to 0.525f, + Pair(20f, 2.2f) to 0.550f, + Pair(20f, 2.3f) to 0.575f, + Pair(20f, 2.4f) to 0.600f, + Pair(20f, 2.5f) to 0.625f, + Pair(20f, 2.6f) to 0.650f, + Pair(20f, 2.7f) to 0.675f, + Pair(20f, 2.8f) to 0.700f, + Pair(20f, 2.9f) to 0.725f, + Pair(20f, 3.0f) to 0.750f, + Pair(20f, 3.2f) to 0.800f, + Pair(20f, 3.4f) to 0.850f, + Pair(20f, 3.6f) to 0.900f, + Pair(20f, 3.8f) to 0.950f, + Pair(20f, 4.0f) to 1.000f, + Pair(20f, 4.2f) to 1.050f, + Pair(20f, 4.4f) to 1.100f, + Pair(20f, 4.6f) to 1.150f, + Pair(20f, 4.8f) to 1.200f, + Pair(20f, 5.0f) to 1.250f, + Pair(20f, 5.5f) to 1.375f, + Pair(20f, 6.0f) to 1.500f, + Pair(20f, 6.5f) to 1.625f, + Pair(20f, 7.0f) to 1.750f, + Pair(20f, 7.5f) to 1.875f, + Pair(20f, 8.0f) to 2.000f, + Pair(20f, 8.5f) to 2.125f, + Pair(20f, 9.0f) to 2.250f, + Pair(20f, 9.5f) to 2.375f, + Pair(20f, 10.0f) to 2.500f, + Pair(20f, 11.0f) to 2.750f, + Pair(20f, 12.0f) to 3.000f, + Pair(20f, 13.0f) to 3.250f, + Pair(20f, 14.0f) to 3.500f, + Pair(20f, 15.0f) to 3.750f, + Pair(20f, 16.0f) to 4.000f, + Pair(20f, 17.0f) to 4.250f, + Pair(20f, 18.0f) to 4.500f, + Pair(20f, 19.0f) to 4.750f, + Pair(20f, 20.0f) to 5.000f, + Pair(20f, 21.0f) to 5.250f, + Pair(20f, 22.0f) to 5.500f, + Pair(20f, 23.0f) to 5.750f, + Pair(20f, 24.0f) to 6.000f, + Pair(20f, 25.0f) to 6.250f, + Pair(20f, 26.0f) to 6.500f, + Pair(20f, 27.0f) to 6.750f, + Pair(20f, 28.0f) to 7.000f, + Pair(20f, 29.0f) to 7.250f, + Pair(20f, 30.0f) to 7.500f, + Pair(20f, 31.0f) to 7.750f, + Pair(20f, 32.0f) to 8.000f, + Pair(20f, 33.0f) to 8.250f, + Pair(20f, 34.0f) to 8.500f, + Pair(20f, 35.0f) to 8.750f, + Pair(20f, 36.0f) to 9.000f, + Pair(20f, 37.0f) to 9.250f, + Pair(20f, 38.0f) to 9.500f, + Pair(20f, 39.0f) to 9.750f, + Pair(20f, 40.0f) to 10.000f, + + // Pulse Width = 25f + Pair(25f, 2.0f) to 0.500f, + Pair(25f, 2.1f) to 0.525f, + Pair(25f, 2.2f) to 0.550f, + Pair(25f, 2.3f) to 0.575f, + Pair(25f, 2.4f) to 0.600f, + Pair(25f, 2.5f) to 0.625f, + Pair(25f, 2.6f) to 0.650f, + Pair(25f, 2.7f) to 0.675f, + Pair(25f, 2.8f) to 0.700f, + Pair(25f, 2.9f) to 0.725f, + Pair(25f, 3.0f) to 0.750f, + Pair(25f, 3.2f) to 0.800f, + Pair(25f, 3.4f) to 0.850f, + Pair(25f, 3.6f) to 0.900f, + Pair(25f, 3.8f) to 0.950f, + Pair(25f, 4.0f) to 1.000f, + Pair(25f, 4.2f) to 1.050f, + Pair(25f, 4.4f) to 1.100f, + Pair(25f, 4.6f) to 1.150f, + Pair(25f, 4.8f) to 1.200f, + Pair(25f, 5.0f) to 1.250f, + Pair(25f, 5.5f) to 1.375f, + Pair(25f, 6.0f) to 1.500f, + Pair(25f, 6.5f) to 1.625f, + Pair(25f, 7.0f) to 1.750f, + Pair(25f, 7.5f) to 1.875f, + Pair(25f, 8.0f) to 2.000f, + Pair(25f, 8.5f) to 2.125f, + Pair(25f, 9.0f) to 2.250f, + Pair(25f, 9.5f) to 2.375f, + Pair(25f, 10.0f) to 2.500f, + Pair(25f, 11.0f) to 2.750f, + Pair(25f, 12.0f) to 3.000f, + Pair(25f, 13.0f) to 3.250f, + Pair(25f, 14.0f) to 3.500f, + Pair(25f, 15.0f) to 3.750f, + Pair(25f, 16.0f) to 4.000f, + Pair(25f, 17.0f) to 4.250f, + Pair(25f, 18.0f) to 4.500f, + Pair(25f, 19.0f) to 4.750f, + Pair(25f, 20.0f) to 5.000f, + Pair(25f, 21.0f) to 5.250f, + Pair(25f, 22.0f) to 5.500f, + Pair(25f, 23.0f) to 5.750f, + Pair(25f, 24.0f) to 6.000f, + Pair(25f, 25.0f) to 6.250f, + Pair(25f, 26.0f) to 6.500f, + Pair(25f, 27.0f) to 6.750f, + Pair(25f, 28.0f) to 7.000f, + Pair(25f, 29.0f) to 7.250f, + Pair(25f, 30.0f) to 7.500f, + Pair(25f, 31.0f) to 7.750f, + Pair(25f, 32.0f) to 8.000f, + Pair(25f, 33.0f) to 8.250f, + Pair(25f, 34.0f) to 8.500f, + Pair(25f, 35.0f) to 8.750f, + Pair(25f, 36.0f) to 9.000f, + Pair(25f, 37.0f) to 9.250f, + Pair(25f, 38.0f) to 9.500f, + Pair(25f, 39.0f) to 9.750f, + Pair(25f, 40.0f) to 10.000f, + + // Pulse Width = 30f + Pair(30f, 2.0f) to 0.500f, + Pair(30f, 2.1f) to 0.525f, + Pair(30f, 2.2f) to 0.550f, + Pair(30f, 2.3f) to 0.575f, + Pair(30f, 2.4f) to 0.600f, + Pair(30f, 2.5f) to 0.625f, + Pair(30f, 2.6f) to 0.650f, + Pair(30f, 2.7f) to 0.675f, + Pair(30f, 2.8f) to 0.700f, + Pair(30f, 2.9f) to 0.725f, + Pair(30f, 3.0f) to 0.750f, + Pair(30f, 3.2f) to 0.800f, + Pair(30f, 3.4f) to 0.850f, + Pair(30f, 3.6f) to 0.900f, + Pair(30f, 3.8f) to 0.950f, + Pair(30f, 4.0f) to 1.000f, + Pair(30f, 4.2f) to 1.050f, + Pair(30f, 4.4f) to 1.100f, + Pair(30f, 4.6f) to 1.150f, + Pair(30f, 4.8f) to 1.200f, + Pair(30f, 5.0f) to 1.250f, + Pair(30f, 5.5f) to 1.375f, + Pair(30f, 6.0f) to 1.500f, + Pair(30f, 6.5f) to 1.625f, + Pair(30f, 7.0f) to 1.750f, + Pair(30f, 7.5f) to 1.875f, + Pair(30f, 8.0f) to 2.000f, + Pair(30f, 8.5f) to 2.125f, + Pair(30f, 9.0f) to 2.250f, + Pair(30f, 9.5f) to 2.375f, + Pair(30f, 10.0f) to 2.500f, + Pair(30f, 11.0f) to 2.750f, + Pair(30f, 12.0f) to 3.000f, + Pair(30f, 13.0f) to 3.250f, + Pair(30f, 14.0f) to 3.500f, + Pair(30f, 15.0f) to 3.750f, + Pair(30f, 16.0f) to 4.000f, + Pair(30f, 17.0f) to 4.250f, + Pair(30f, 18.0f) to 4.500f, + Pair(30f, 19.0f) to 4.750f, + Pair(30f, 20.0f) to 5.000f, + Pair(30f, 21.0f) to 5.250f, + Pair(30f, 22.0f) to 5.500f, + Pair(30f, 23.0f) to 5.750f, + Pair(30f, 24.0f) to 6.000f, + Pair(30f, 25.0f) to 6.250f, + Pair(30f, 26.0f) to 6.500f, + Pair(30f, 27.0f) to 6.750f, + Pair(30f, 28.0f) to 7.000f, + Pair(30f, 29.0f) to 7.250f, + Pair(30f, 30.0f) to 7.500f, + Pair(30f, 31.0f) to 7.750f, + Pair(30f, 32.0f) to 8.000f, + Pair(30f, 33.0f) to 8.250f, + Pair(30f, 34.0f) to 8.500f, + Pair(30f, 35.0f) to 8.750f, + Pair(30f, 36.0f) to 9.000f, + Pair(30f, 37.0f) to 9.250f, + Pair(30f, 38.0f) to 9.500f, + Pair(30f, 39.0f) to 9.750f, + Pair(30f, 40.0f) to 10.000f, + + // Pulse Width = 35f + Pair(35f, 2.0f) to 0.500f, + Pair(35f, 2.1f) to 0.525f, + Pair(35f, 2.2f) to 0.550f, + Pair(35f, 2.3f) to 0.575f, + Pair(35f, 2.4f) to 0.600f, + Pair(35f, 2.5f) to 0.625f, + Pair(35f, 2.6f) to 0.650f, + Pair(35f, 2.7f) to 0.675f, + Pair(35f, 2.8f) to 0.700f, + Pair(35f, 2.9f) to 0.725f, + Pair(35f, 3.0f) to 0.750f, + Pair(35f, 3.2f) to 0.800f, + Pair(35f, 3.4f) to 0.850f, + Pair(35f, 3.6f) to 0.900f, + Pair(35f, 3.8f) to 0.950f, + Pair(35f, 4.0f) to 1.000f, + Pair(35f, 4.2f) to 1.050f, + Pair(35f, 4.4f) to 1.100f, + Pair(35f, 4.6f) to 1.150f, + Pair(35f, 4.8f) to 1.200f, + Pair(35f, 5.0f) to 1.250f, + Pair(35f, 5.5f) to 1.375f, + Pair(35f, 6.0f) to 1.500f, + Pair(35f, 6.5f) to 1.625f, + Pair(35f, 7.0f) to 1.750f, + Pair(35f, 7.5f) to 1.875f, + Pair(35f, 8.0f) to 2.000f, + Pair(35f, 8.5f) to 2.125f, + Pair(35f, 9.0f) to 2.250f, + Pair(35f, 9.5f) to 2.375f, + Pair(35f, 10.0f) to 2.500f, + Pair(35f, 11.0f) to 2.750f, + Pair(35f, 12.0f) to 3.000f, + Pair(35f, 13.0f) to 3.250f, + Pair(35f, 14.0f) to 3.500f, + Pair(35f, 15.0f) to 3.750f, + Pair(35f, 16.0f) to 4.000f, + Pair(35f, 17.0f) to 4.250f, + Pair(35f, 18.0f) to 4.500f, + Pair(35f, 19.0f) to 4.750f, + Pair(35f, 20.0f) to 5.000f, + Pair(35f, 21.0f) to 5.250f, + Pair(35f, 22.0f) to 5.500f, + Pair(35f, 23.0f) to 5.750f, + Pair(35f, 24.0f) to 6.000f, + Pair(35f, 25.0f) to 6.250f, + Pair(35f, 26.0f) to 6.500f, + Pair(35f, 27.0f) to 6.750f, + Pair(35f, 28.0f) to 7.000f, + Pair(35f, 29.0f) to 7.250f, + Pair(35f, 30.0f) to 7.500f, + Pair(35f, 31.0f) to 7.750f, + Pair(35f, 32.0f) to 8.000f, + Pair(35f, 33.0f) to 8.250f, + Pair(35f, 34.0f) to 8.500f, + Pair(35f, 35.0f) to 8.750f, + Pair(35f, 36.0f) to 9.000f, + Pair(35f, 37.0f) to 9.250f, + Pair(35f, 38.0f) to 9.500f, + Pair(35f, 39.0f) to 9.750f, + Pair(35f, 40.0f) to 10.000f, + + // Pulse Width = 40f + Pair(40f, 2.0f) to 0.500f, + Pair(40f, 2.1f) to 0.525f, + Pair(40f, 2.2f) to 0.550f, + Pair(40f, 2.3f) to 0.575f, + Pair(40f, 2.4f) to 0.600f, + Pair(40f, 2.5f) to 0.625f, + Pair(40f, 2.6f) to 0.650f, + Pair(40f, 2.7f) to 0.675f, + Pair(40f, 2.8f) to 0.700f, + Pair(40f, 2.9f) to 0.725f, + Pair(40f, 3.0f) to 0.750f, + Pair(40f, 3.2f) to 0.800f, + Pair(40f, 3.4f) to 0.850f, + Pair(40f, 3.6f) to 0.900f, + Pair(40f, 3.8f) to 0.950f, + Pair(40f, 4.0f) to 1.000f, + Pair(40f, 4.2f) to 1.050f, + Pair(40f, 4.4f) to 1.100f, + Pair(40f, 4.6f) to 1.150f, + Pair(40f, 4.8f) to 1.200f, + Pair(40f, 5.0f) to 1.250f, + Pair(40f, 5.5f) to 1.375f, + Pair(40f, 6.0f) to 1.500f, + Pair(40f, 6.5f) to 1.625f, + Pair(40f, 7.0f) to 1.750f, + Pair(40f, 7.5f) to 1.875f, + Pair(40f, 8.0f) to 2.000f, + Pair(40f, 8.5f) to 2.125f, + Pair(40f, 9.0f) to 2.250f, + Pair(40f, 9.5f) to 2.375f, + Pair(40f, 10.0f) to 2.500f, + Pair(40f, 11.0f) to 2.750f, + Pair(40f, 12.0f) to 3.000f, + Pair(40f, 13.0f) to 3.250f, + Pair(40f, 14.0f) to 3.500f, + Pair(40f, 15.0f) to 3.750f, + Pair(40f, 16.0f) to 4.000f, + Pair(40f, 17.0f) to 4.250f, + Pair(40f, 18.0f) to 4.500f, + Pair(40f, 19.0f) to 4.750f, + Pair(40f, 20.0f) to 5.000f, + Pair(40f, 21.0f) to 5.250f, + Pair(40f, 22.0f) to 5.500f, + Pair(40f, 23.0f) to 5.750f, + Pair(40f, 24.0f) to 6.000f, + Pair(40f, 25.0f) to 6.250f, + Pair(40f, 26.0f) to 6.500f, + Pair(40f, 27.0f) to 6.750f, + Pair(40f, 28.0f) to 7.000f, + Pair(40f, 29.0f) to 7.250f, + Pair(40f, 30.0f) to 7.500f, + Pair(40f, 31.0f) to 7.750f, + Pair(40f, 32.0f) to 8.000f, + Pair(40f, 33.0f) to 8.250f, + Pair(40f, 34.0f) to 8.500f, + Pair(40f, 35.0f) to 8.750f, + Pair(40f, 36.0f) to 9.000f, + Pair(40f, 37.0f) to 9.250f, + Pair(40f, 38.0f) to 9.500f, + Pair(40f, 39.0f) to 9.750f, + Pair(40f, 40.0f) to 10.000f, +) + diff --git a/app/src/main/java/com/laseroptek/raman/const/EnergyTable_10_10.kt b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_10_10.kt new file mode 100644 index 0000000..73d60ae --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_10_10.kt @@ -0,0 +1,509 @@ +package com.laseroptek.raman.const + +// +// Energy Table3 (HANDPIENCE-10x10) +// - Map, Float> +// - Map, VALUE> +// - Map, Energy(J)> +// + +val EnergyTable_10_10 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 0.5f) to 0.500f, + Pair(0.5f, 0.6f) to 0.600f, + Pair(0.5f, 0.7f) to 0.700f, + Pair(0.5f, 0.8f) to 0.800f, + Pair(0.5f, 0.9f) to 0.900f, + Pair(0.5f, 1.0f) to 1.000f, + Pair(0.5f, 1.1f) to 1.100f, + Pair(0.5f, 1.2f) to 1.200f, + Pair(0.5f, 1.3f) to 1.300f, + Pair(0.5f, 1.4f) to 1.400f, + Pair(0.5f, 1.5f) to 1.500f, + Pair(0.5f, 1.6f) to 1.600f, + Pair(0.5f, 1.7f) to 1.700f, + Pair(0.5f, 1.8f) to 1.800f, + Pair(0.5f, 1.9f) to 1.900f, + Pair(0.5f, 2.0f) to 2.000f, + + // Pulse Width = 1f + Pair(1f, 0.5f) to 0.500f, + Pair(1f, 0.6f) to 0.600f, + Pair(1f, 0.7f) to 0.700f, + Pair(1f, 0.8f) to 0.800f, + Pair(1f, 0.9f) to 0.900f, + Pair(1f, 1.0f) to 1.000f, + Pair(1f, 1.1f) to 1.100f, + Pair(1f, 1.2f) to 1.200f, + Pair(1f, 1.3f) to 1.300f, + Pair(1f, 1.4f) to 1.400f, + Pair(1f, 1.5f) to 1.500f, + Pair(1f, 1.6f) to 1.600f, + Pair(1f, 1.7f) to 1.700f, + Pair(1f, 1.8f) to 1.800f, + Pair(1f, 1.9f) to 1.900f, + Pair(1f, 2.0f) to 2.000f, + Pair(1f, 2.1f) to 2.100f, + Pair(1f, 2.2f) to 2.200f, + Pair(1f, 2.3f) to 2.300f, + Pair(1f, 2.4f) to 2.400f, + Pair(1f, 2.5f) to 2.500f, + Pair(1f, 2.6f) to 2.600f, + Pair(1f, 2.7f) to 2.700f, + Pair(1f, 2.8f) to 2.800f, + Pair(1f, 2.9f) to 2.900f, + Pair(1f, 3.0f) to 3.000f, + + // Pulse Width = 1.5f + Pair(1.5f, 0.5f) to 0.500f, + Pair(1.5f, 0.6f) to 0.600f, + Pair(1.5f, 0.7f) to 0.700f, + Pair(1.5f, 0.8f) to 0.800f, + Pair(1.5f, 0.9f) to 0.900f, + Pair(1.5f, 1.0f) to 1.000f, + Pair(1.5f, 1.1f) to 1.100f, + Pair(1.5f, 1.2f) to 1.200f, + Pair(1.5f, 1.3f) to 1.300f, + Pair(1.5f, 1.4f) to 1.400f, + Pair(1.5f, 1.5f) to 1.500f, + Pair(1.5f, 1.6f) to 1.600f, + Pair(1.5f, 1.7f) to 1.700f, + Pair(1.5f, 1.8f) to 1.800f, + Pair(1.5f, 1.9f) to 1.900f, + Pair(1.5f, 2.0f) to 2.000f, + Pair(1.5f, 2.1f) to 2.100f, + Pair(1.5f, 2.2f) to 2.200f, + Pair(1.5f, 2.3f) to 2.300f, + Pair(1.5f, 2.4f) to 2.400f, + Pair(1.5f, 2.5f) to 2.500f, + Pair(1.5f, 2.6f) to 2.600f, + Pair(1.5f, 2.7f) to 2.700f, + Pair(1.5f, 2.8f) to 2.800f, + Pair(1.5f, 2.9f) to 2.900f, + Pair(1.5f, 3.0f) to 3.000f, + Pair(1.5f, 3.2f) to 3.200f, + Pair(1.5f, 3.4f) to 3.400f, + Pair(1.5f, 3.6f) to 3.600f, + Pair(1.5f, 3.8f) to 3.800f, + Pair(1.5f, 4.0f) to 4.000f, + + // Pulse Width = 3f + Pair(3f, 0.5f) to 0.500f, + Pair(3f, 0.6f) to 0.600f, + Pair(3f, 0.7f) to 0.700f, + Pair(3f, 0.8f) to 0.800f, + Pair(3f, 0.9f) to 0.900f, + Pair(3f, 1.0f) to 1.000f, + Pair(3f, 1.1f) to 1.100f, + Pair(3f, 1.2f) to 1.200f, + Pair(3f, 1.3f) to 1.300f, + Pair(3f, 1.4f) to 1.400f, + Pair(3f, 1.5f) to 1.500f, + Pair(3f, 1.6f) to 1.600f, + Pair(3f, 1.7f) to 1.700f, + Pair(3f, 1.8f) to 1.800f, + Pair(3f, 1.9f) to 1.900f, + Pair(3f, 2.0f) to 2.000f, + Pair(3f, 2.1f) to 2.100f, + Pair(3f, 2.2f) to 2.200f, + Pair(3f, 2.3f) to 2.300f, + Pair(3f, 2.4f) to 2.400f, + Pair(3f, 2.5f) to 2.500f, + Pair(3f, 2.6f) to 2.600f, + Pair(3f, 2.7f) to 2.700f, + Pair(3f, 2.8f) to 2.800f, + Pair(3f, 2.9f) to 2.900f, + Pair(3f, 3.0f) to 3.000f, + Pair(3f, 3.2f) to 3.200f, + Pair(3f, 3.4f) to 3.400f, + Pair(3f, 3.6f) to 3.600f, + Pair(3f, 3.8f) to 3.800f, + Pair(3f, 4.0f) to 4.000f, + Pair(3f, 4.2f) to 4.200f, + Pair(3f, 4.4f) to 4.400f, + Pair(3f, 4.6f) to 4.600f, + Pair(3f, 4.8f) to 4.800f, + Pair(3f, 5.0f) to 5.000f, + Pair(3f, 5.5f) to 5.500f, + Pair(3f, 6.0f) to 6.000f, + + // Pulse Width = 5f + Pair(5f, 0.5f) to 0.500f, + Pair(5f, 0.6f) to 0.600f, + Pair(5f, 0.7f) to 0.700f, + Pair(5f, 0.8f) to 0.800f, + Pair(5f, 0.9f) to 0.900f, + Pair(5f, 1.0f) to 1.000f, + Pair(5f, 1.1f) to 1.100f, + Pair(5f, 1.2f) to 1.200f, + Pair(5f, 1.3f) to 1.300f, + Pair(5f, 1.4f) to 1.400f, + Pair(5f, 1.5f) to 1.500f, + Pair(5f, 1.6f) to 1.600f, + Pair(5f, 1.7f) to 1.700f, + Pair(5f, 1.8f) to 1.800f, + Pair(5f, 1.9f) to 1.900f, + Pair(5f, 2.0f) to 2.000f, + Pair(5f, 2.1f) to 2.100f, + Pair(5f, 2.2f) to 2.200f, + Pair(5f, 2.3f) to 2.300f, + Pair(5f, 2.4f) to 2.400f, + Pair(5f, 2.5f) to 2.500f, + Pair(5f, 2.6f) to 2.600f, + Pair(5f, 2.7f) to 2.700f, + Pair(5f, 2.8f) to 2.800f, + Pair(5f, 2.9f) to 2.900f, + Pair(5f, 3.0f) to 3.000f, + Pair(5f, 3.2f) to 3.200f, + Pair(5f, 3.4f) to 3.400f, + Pair(5f, 3.6f) to 3.600f, + Pair(5f, 3.8f) to 3.800f, + Pair(5f, 4.0f) to 4.000f, + Pair(5f, 4.2f) to 4.200f, + Pair(5f, 4.4f) to 4.400f, + Pair(5f, 4.6f) to 4.600f, + Pair(5f, 4.8f) to 4.800f, + Pair(5f, 5.0f) to 5.000f, + Pair(5f, 5.5f) to 5.500f, + Pair(5f, 6.0f) to 6.000f, + Pair(5f, 6.5f) to 6.500f, + Pair(5f, 7.0f) to 7.000f, + + // Pulse Width = 10f + Pair(10f, 0.5f) to 0.500f, + Pair(10f, 0.6f) to 0.600f, + Pair(10f, 0.7f) to 0.700f, + Pair(10f, 0.8f) to 0.800f, + Pair(10f, 0.9f) to 0.900f, + Pair(10f, 1.0f) to 1.000f, + Pair(10f, 1.1f) to 1.100f, + Pair(10f, 1.2f) to 1.200f, + Pair(10f, 1.3f) to 1.300f, + Pair(10f, 1.4f) to 1.400f, + Pair(10f, 1.5f) to 1.500f, + Pair(10f, 1.6f) to 1.600f, + Pair(10f, 1.7f) to 1.700f, + Pair(10f, 1.8f) to 1.800f, + Pair(10f, 1.9f) to 1.900f, + Pair(10f, 2.0f) to 2.000f, + Pair(10f, 2.1f) to 2.100f, + Pair(10f, 2.2f) to 2.200f, + Pair(10f, 2.3f) to 2.300f, + Pair(10f, 2.4f) to 2.400f, + Pair(10f, 2.5f) to 2.500f, + Pair(10f, 2.6f) to 2.600f, + Pair(10f, 2.7f) to 2.700f, + Pair(10f, 2.8f) to 2.800f, + Pair(10f, 2.9f) to 2.900f, + Pair(10f, 3.0f) to 3.000f, + Pair(10f, 3.2f) to 3.200f, + Pair(10f, 3.4f) to 3.400f, + Pair(10f, 3.6f) to 3.600f, + Pair(10f, 3.8f) to 3.800f, + Pair(10f, 4.0f) to 4.000f, + Pair(10f, 4.2f) to 4.200f, + Pair(10f, 4.4f) to 4.400f, + Pair(10f, 4.6f) to 4.600f, + Pair(10f, 4.8f) to 4.800f, + Pair(10f, 5.0f) to 5.000f, + Pair(10f, 5.5f) to 5.500f, + Pair(10f, 6.0f) to 6.000f, + Pair(10f, 6.5f) to 6.500f, + Pair(10f, 7.0f) to 7.000f, + Pair(10f, 7.5f) to 7.500f, + Pair(10f, 8.0f) to 8.000f, + Pair(10f, 8.5f) to 8.500f, + Pair(10f, 9.0f) to 9.000f, + Pair(10f, 9.5f) to 9.500f, + Pair(10f, 10.0f) to 10.000f, + + // Pulse Width = 15f + Pair(15f, 0.5f) to 0.500f, + Pair(15f, 0.6f) to 0.600f, + Pair(15f, 0.7f) to 0.700f, + Pair(15f, 0.8f) to 0.800f, + Pair(15f, 0.9f) to 0.900f, + Pair(15f, 1.0f) to 1.000f, + Pair(15f, 1.1f) to 1.100f, + Pair(15f, 1.2f) to 1.200f, + Pair(15f, 1.3f) to 1.300f, + Pair(15f, 1.4f) to 1.400f, + Pair(15f, 1.5f) to 1.500f, + Pair(15f, 1.6f) to 1.600f, + Pair(15f, 1.7f) to 1.700f, + Pair(15f, 1.8f) to 1.800f, + Pair(15f, 1.9f) to 1.900f, + Pair(15f, 2.0f) to 2.000f, + Pair(15f, 2.1f) to 2.100f, + Pair(15f, 2.2f) to 2.200f, + Pair(15f, 2.3f) to 2.300f, + Pair(15f, 2.4f) to 2.400f, + Pair(15f, 2.5f) to 2.500f, + Pair(15f, 2.6f) to 2.600f, + Pair(15f, 2.7f) to 2.700f, + Pair(15f, 2.8f) to 2.800f, + Pair(15f, 2.9f) to 2.900f, + Pair(15f, 3.0f) to 3.000f, + Pair(15f, 3.2f) to 3.200f, + Pair(15f, 3.4f) to 3.400f, + Pair(15f, 3.6f) to 3.600f, + Pair(15f, 3.8f) to 3.800f, + Pair(15f, 4.0f) to 4.000f, + Pair(15f, 4.2f) to 4.200f, + Pair(15f, 4.4f) to 4.400f, + Pair(15f, 4.6f) to 4.600f, + Pair(15f, 4.8f) to 4.800f, + Pair(15f, 5.0f) to 5.000f, + Pair(15f, 5.5f) to 5.500f, + Pair(15f, 6.0f) to 6.000f, + Pair(15f, 6.5f) to 6.500f, + Pair(15f, 7.0f) to 7.000f, + Pair(15f, 7.5f) to 7.500f, + Pair(15f, 8.0f) to 8.000f, + Pair(15f, 8.5f) to 8.500f, + Pair(15f, 9.0f) to 9.000f, + Pair(15f, 9.5f) to 9.500f, + Pair(15f, 10.0f) to 10.000f, + + // Pulse Width = 20f + Pair(20f, 0.5f) to 0.500f, + Pair(20f, 0.6f) to 0.600f, + Pair(20f, 0.7f) to 0.700f, + Pair(20f, 0.8f) to 0.800f, + Pair(20f, 0.9f) to 0.900f, + Pair(20f, 1.0f) to 1.000f, + Pair(20f, 1.1f) to 1.100f, + Pair(20f, 1.2f) to 1.200f, + Pair(20f, 1.3f) to 1.300f, + Pair(20f, 1.4f) to 1.400f, + Pair(20f, 1.5f) to 1.500f, + Pair(20f, 1.6f) to 1.600f, + Pair(20f, 1.7f) to 1.700f, + Pair(20f, 1.8f) to 1.800f, + Pair(20f, 1.9f) to 1.900f, + Pair(20f, 2.0f) to 2.000f, + Pair(20f, 2.1f) to 2.100f, + Pair(20f, 2.2f) to 2.200f, + Pair(20f, 2.3f) to 2.300f, + Pair(20f, 2.4f) to 2.400f, + Pair(20f, 2.5f) to 2.500f, + Pair(20f, 2.6f) to 2.600f, + Pair(20f, 2.7f) to 2.700f, + Pair(20f, 2.8f) to 2.800f, + Pair(20f, 2.9f) to 2.900f, + Pair(20f, 3.0f) to 3.000f, + Pair(20f, 3.2f) to 3.200f, + Pair(20f, 3.4f) to 3.400f, + Pair(20f, 3.6f) to 3.600f, + Pair(20f, 3.8f) to 3.800f, + Pair(20f, 4.0f) to 4.000f, + Pair(20f, 4.2f) to 4.200f, + Pair(20f, 4.4f) to 4.400f, + Pair(20f, 4.6f) to 4.600f, + Pair(20f, 4.8f) to 4.800f, + Pair(20f, 5.0f) to 5.000f, + Pair(20f, 5.5f) to 5.500f, + Pair(20f, 6.0f) to 6.000f, + Pair(20f, 6.5f) to 6.500f, + Pair(20f, 7.0f) to 7.000f, + Pair(20f, 7.5f) to 7.500f, + Pair(20f, 8.0f) to 8.000f, + Pair(20f, 8.5f) to 8.500f, + Pair(20f, 9.0f) to 9.000f, + Pair(20f, 9.5f) to 9.500f, + Pair(20f, 10.0f) to 10.000f, + + // Pulse Width = 25f + Pair(25f, 0.5f) to 0.500f, + Pair(25f, 0.6f) to 0.600f, + Pair(25f, 0.7f) to 0.700f, + Pair(25f, 0.8f) to 0.800f, + Pair(25f, 0.9f) to 0.900f, + Pair(25f, 1.0f) to 1.000f, + Pair(25f, 1.1f) to 1.100f, + Pair(25f, 1.2f) to 1.200f, + Pair(25f, 1.3f) to 1.300f, + Pair(25f, 1.4f) to 1.400f, + Pair(25f, 1.5f) to 1.500f, + Pair(25f, 1.6f) to 1.600f, + Pair(25f, 1.7f) to 1.700f, + Pair(25f, 1.8f) to 1.800f, + Pair(25f, 1.9f) to 1.900f, + Pair(25f, 2.0f) to 2.000f, + Pair(25f, 2.1f) to 2.100f, + Pair(25f, 2.2f) to 2.200f, + Pair(25f, 2.3f) to 2.300f, + Pair(25f, 2.4f) to 2.400f, + Pair(25f, 2.5f) to 2.500f, + Pair(25f, 2.6f) to 2.600f, + Pair(25f, 2.7f) to 2.700f, + Pair(25f, 2.8f) to 2.800f, + Pair(25f, 2.9f) to 2.900f, + Pair(25f, 3.0f) to 3.000f, + Pair(25f, 3.2f) to 3.200f, + Pair(25f, 3.4f) to 3.400f, + Pair(25f, 3.6f) to 3.600f, + Pair(25f, 3.8f) to 3.800f, + Pair(25f, 4.0f) to 4.000f, + Pair(25f, 4.2f) to 4.200f, + Pair(25f, 4.4f) to 4.400f, + Pair(25f, 4.6f) to 4.600f, + Pair(25f, 4.8f) to 4.800f, + Pair(25f, 5.0f) to 5.000f, + Pair(25f, 5.5f) to 5.500f, + Pair(25f, 6.0f) to 6.000f, + Pair(25f, 6.5f) to 6.500f, + Pair(25f, 7.0f) to 7.000f, + Pair(25f, 7.5f) to 7.500f, + Pair(25f, 8.0f) to 8.000f, + Pair(25f, 8.5f) to 8.500f, + Pair(25f, 9.0f) to 9.000f, + Pair(25f, 9.5f) to 9.500f, + Pair(25f, 10.0f) to 10.000f, + + // Pulse Width = 30f + Pair(30f, 0.5f) to 0.500f, + Pair(30f, 0.6f) to 0.600f, + Pair(30f, 0.7f) to 0.700f, + Pair(30f, 0.8f) to 0.800f, + Pair(30f, 0.9f) to 0.900f, + Pair(30f, 1.0f) to 1.000f, + Pair(30f, 1.1f) to 1.100f, + Pair(30f, 1.2f) to 1.200f, + Pair(30f, 1.3f) to 1.300f, + Pair(30f, 1.4f) to 1.400f, + Pair(30f, 1.5f) to 1.500f, + Pair(30f, 1.6f) to 1.600f, + Pair(30f, 1.7f) to 1.700f, + Pair(30f, 1.8f) to 1.800f, + Pair(30f, 1.9f) to 1.900f, + Pair(30f, 2.0f) to 2.000f, + Pair(30f, 2.1f) to 2.100f, + Pair(30f, 2.2f) to 2.200f, + Pair(30f, 2.3f) to 2.300f, + Pair(30f, 2.4f) to 2.400f, + Pair(30f, 2.5f) to 2.500f, + Pair(30f, 2.6f) to 2.600f, + Pair(30f, 2.7f) to 2.700f, + Pair(30f, 2.8f) to 2.800f, + Pair(30f, 2.9f) to 2.900f, + Pair(30f, 3.0f) to 3.000f, + Pair(30f, 3.2f) to 3.200f, + Pair(30f, 3.4f) to 3.400f, + Pair(30f, 3.6f) to 3.600f, + Pair(30f, 3.8f) to 3.800f, + Pair(30f, 4.0f) to 4.000f, + Pair(30f, 4.2f) to 4.200f, + Pair(30f, 4.4f) to 4.400f, + Pair(30f, 4.6f) to 4.600f, + Pair(30f, 4.8f) to 4.800f, + Pair(30f, 5.0f) to 5.000f, + Pair(30f, 5.5f) to 5.500f, + Pair(30f, 6.0f) to 6.000f, + Pair(30f, 6.5f) to 6.500f, + Pair(30f, 7.0f) to 7.000f, + Pair(30f, 7.5f) to 7.500f, + Pair(30f, 8.0f) to 8.000f, + Pair(30f, 8.5f) to 8.500f, + Pair(30f, 9.0f) to 9.000f, + Pair(30f, 9.5f) to 9.500f, + Pair(30f, 10.0f) to 10.000f, + + // Pulse Width = 35f + Pair(35f, 0.5f) to 0.500f, + Pair(35f, 0.6f) to 0.600f, + Pair(35f, 0.7f) to 0.700f, + Pair(35f, 0.8f) to 0.800f, + Pair(35f, 0.9f) to 0.900f, + Pair(35f, 1.0f) to 1.000f, + Pair(35f, 1.1f) to 1.100f, + Pair(35f, 1.2f) to 1.200f, + Pair(35f, 1.3f) to 1.300f, + Pair(35f, 1.4f) to 1.400f, + Pair(35f, 1.5f) to 1.500f, + Pair(35f, 1.6f) to 1.600f, + Pair(35f, 1.7f) to 1.700f, + Pair(35f, 1.8f) to 1.800f, + Pair(35f, 1.9f) to 1.900f, + Pair(35f, 2.0f) to 2.000f, + Pair(35f, 2.1f) to 2.100f, + Pair(35f, 2.2f) to 2.200f, + Pair(35f, 2.3f) to 2.300f, + Pair(35f, 2.4f) to 2.400f, + Pair(35f, 2.5f) to 2.500f, + Pair(35f, 2.6f) to 2.600f, + Pair(35f, 2.7f) to 2.700f, + Pair(35f, 2.8f) to 2.800f, + Pair(35f, 2.9f) to 2.900f, + Pair(35f, 3.0f) to 3.000f, + Pair(35f, 3.2f) to 3.200f, + Pair(35f, 3.4f) to 3.400f, + Pair(35f, 3.6f) to 3.600f, + Pair(35f, 3.8f) to 3.800f, + Pair(35f, 4.0f) to 4.000f, + Pair(35f, 4.2f) to 4.200f, + Pair(35f, 4.4f) to 4.400f, + Pair(35f, 4.6f) to 4.600f, + Pair(35f, 4.8f) to 4.800f, + Pair(35f, 5.0f) to 5.000f, + Pair(35f, 5.5f) to 5.500f, + Pair(35f, 6.0f) to 6.000f, + Pair(35f, 6.5f) to 6.500f, + Pair(35f, 7.0f) to 7.000f, + Pair(35f, 7.5f) to 7.500f, + Pair(35f, 8.0f) to 8.000f, + Pair(35f, 8.5f) to 8.500f, + Pair(35f, 9.0f) to 9.000f, + Pair(35f, 9.5f) to 9.500f, + Pair(35f, 10.0f) to 10.000f, + + // Pulse Width = 40f + Pair(40f, 0.5f) to 0.500f, + Pair(40f, 0.6f) to 0.600f, + Pair(40f, 0.7f) to 0.700f, + Pair(40f, 0.8f) to 0.800f, + Pair(40f, 0.9f) to 0.900f, + Pair(40f, 1.0f) to 1.000f, + Pair(40f, 1.1f) to 1.100f, + Pair(40f, 1.2f) to 1.200f, + Pair(40f, 1.3f) to 1.300f, + Pair(40f, 1.4f) to 1.400f, + Pair(40f, 1.5f) to 1.500f, + Pair(40f, 1.6f) to 1.600f, + Pair(40f, 1.7f) to 1.700f, + Pair(40f, 1.8f) to 1.800f, + Pair(40f, 1.9f) to 1.900f, + Pair(40f, 2.0f) to 2.000f, + Pair(40f, 2.1f) to 2.100f, + Pair(40f, 2.2f) to 2.200f, + Pair(40f, 2.3f) to 2.300f, + Pair(40f, 2.4f) to 2.400f, + Pair(40f, 2.5f) to 2.500f, + Pair(40f, 2.6f) to 2.600f, + Pair(40f, 2.7f) to 2.700f, + Pair(40f, 2.8f) to 2.800f, + Pair(40f, 2.9f) to 2.900f, + Pair(40f, 3.0f) to 3.000f, + Pair(40f, 3.2f) to 3.200f, + Pair(40f, 3.4f) to 3.400f, + Pair(40f, 3.6f) to 3.600f, + Pair(40f, 3.8f) to 3.800f, + Pair(40f, 4.0f) to 4.000f, + Pair(40f, 4.2f) to 4.200f, + Pair(40f, 4.4f) to 4.400f, + Pair(40f, 4.6f) to 4.600f, + Pair(40f, 4.8f) to 4.800f, + Pair(40f, 5.0f) to 5.000f, + Pair(40f, 5.5f) to 5.500f, + Pair(40f, 6.0f) to 6.000f, + Pair(40f, 6.5f) to 6.500f, + Pair(40f, 7.0f) to 7.000f, + Pair(40f, 7.5f) to 7.500f, + Pair(40f, 8.0f) to 8.000f, + Pair(40f, 8.5f) to 8.500f, + Pair(40f, 9.0f) to 9.000f, + Pair(40f, 9.5f) to 9.500f, + Pair(40f, 10.0f) to 10.000f, + + ) + diff --git a/app/src/main/java/com/laseroptek/raman/const/EnergyTable_12_12.kt b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_12_12.kt new file mode 100644 index 0000000..e4f125d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_12_12.kt @@ -0,0 +1,435 @@ +package com.laseroptek.raman.const + +// +// Energy Table4 (HANDPIENCE-12x12) +// - Map, Float> +// - Map, VALUE> +// - Map, Energy(J)> +// + +val EnergyTable_12_12 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 0.4f) to 0.576f, + Pair(0.5f, 0.5f) to 0.720f, + Pair(0.5f, 0.6f) to 0.864f, + Pair(0.5f, 0.7f) to 1.008f, + Pair(0.5f, 0.8f) to 1.152f, + Pair(0.5f, 0.9f) to 1.296f, + Pair(0.5f, 1.0f) to 1.440f, + Pair(0.5f, 1.1f) to 1.584f, + Pair(0.5f, 1.2f) to 1.728f, + Pair(0.5f, 1.3f) to 1.872f, + + // Pulse Width = 1f + Pair(1f, 0.4f) to 0.576f, + Pair(1f, 0.5f) to 0.720f, + Pair(1f, 0.6f) to 0.864f, + Pair(1f, 0.7f) to 1.008f, + Pair(1f, 0.8f) to 1.152f, + Pair(1f, 0.9f) to 1.296f, + Pair(1f, 1.0f) to 1.440f, + Pair(1f, 1.1f) to 1.584f, + Pair(1f, 1.2f) to 1.728f, + Pair(1f, 1.3f) to 1.872f, + Pair(1f, 1.4f) to 2.016f, + Pair(1f, 1.5f) to 2.160f, + Pair(1f, 1.6f) to 2.304f, + Pair(1f, 1.7f) to 2.448f, + Pair(1f, 1.8f) to 2.592f, + Pair(1f, 1.9f) to 2.736f, + Pair(1f, 2.0f) to 2.880f, + + // Pulse Width = 1.5f + Pair(1.5f, 0.4f) to 0.576f, + Pair(1.5f, 0.5f) to 0.720f, + Pair(1.5f, 0.6f) to 0.864f, + Pair(1.5f, 0.7f) to 1.008f, + Pair(1.5f, 0.8f) to 1.152f, + Pair(1.5f, 0.9f) to 1.296f, + Pair(1.5f, 1.0f) to 1.440f, + Pair(1.5f, 1.1f) to 1.584f, + Pair(1.5f, 1.2f) to 1.728f, + Pair(1.5f, 1.3f) to 1.872f, + Pair(1.5f, 1.4f) to 2.016f, + Pair(1.5f, 1.5f) to 2.160f, + Pair(1.5f, 1.6f) to 2.304f, + Pair(1.5f, 1.7f) to 2.448f, + Pair(1.5f, 1.8f) to 2.592f, + Pair(1.5f, 1.9f) to 2.736f, + Pair(1.5f, 2.0f) to 2.880f, + Pair(1.5f, 2.1f) to 3.024f, + Pair(1.5f, 2.2f) to 3.168f, + Pair(1.5f, 2.3f) to 3.312f, + Pair(1.5f, 2.4f) to 3.456f, + Pair(1.5f, 2.5f) to 3.600f, + Pair(1.5f, 2.6f) to 3.744f, + Pair(1.5f, 2.7f) to 3.888f, + + // Pulse Width = 3f + Pair(3f, 0.4f) to 0.576f, + Pair(3f, 0.5f) to 0.720f, + Pair(3f, 0.6f) to 0.864f, + Pair(3f, 0.7f) to 1.008f, + Pair(3f, 0.8f) to 1.152f, + Pair(3f, 0.9f) to 1.296f, + Pair(3f, 1.0f) to 1.440f, + Pair(3f, 1.1f) to 1.584f, + Pair(3f, 1.2f) to 1.728f, + Pair(3f, 1.3f) to 1.872f, + Pair(3f, 1.4f) to 2.016f, + Pair(3f, 1.5f) to 2.160f, + Pair(3f, 1.6f) to 2.304f, + Pair(3f, 1.7f) to 2.448f, + Pair(3f, 1.8f) to 2.592f, + Pair(3f, 1.9f) to 2.736f, + Pair(3f, 2.0f) to 2.880f, + Pair(3f, 2.1f) to 3.024f, + Pair(3f, 2.2f) to 3.168f, + Pair(3f, 2.3f) to 3.312f, + Pair(3f, 2.4f) to 3.456f, + Pair(3f, 2.5f) to 3.600f, + Pair(3f, 2.6f) to 3.744f, + Pair(3f, 2.7f) to 3.888f, + Pair(3f, 2.8f) to 4.032f, + Pair(3f, 2.9f) to 4.176f, + Pair(3f, 3.0f) to 4.320f, + Pair(3f, 3.2f) to 4.608f, + Pair(3f, 3.4f) to 4.896f, + Pair(3f, 3.6f) to 5.184f, + Pair(3f, 3.8f) to 5.472f, + Pair(3f, 4.0f) to 5.760f, + + // Pulse Width = 5f + Pair(5f, 0.4f) to 0.576f, + Pair(5f, 0.5f) to 0.720f, + Pair(5f, 0.6f) to 0.864f, + Pair(5f, 0.7f) to 1.008f, + Pair(5f, 0.8f) to 1.152f, + Pair(5f, 0.9f) to 1.296f, + Pair(5f, 1.0f) to 1.440f, + Pair(5f, 1.1f) to 1.584f, + Pair(5f, 1.2f) to 1.728f, + Pair(5f, 1.3f) to 1.872f, + Pair(5f, 1.4f) to 2.016f, + Pair(5f, 1.5f) to 2.160f, + Pair(5f, 1.6f) to 2.304f, + Pair(5f, 1.7f) to 2.448f, + Pair(5f, 1.8f) to 2.592f, + Pair(5f, 1.9f) to 2.736f, + Pair(5f, 2.0f) to 2.880f, + Pair(5f, 2.1f) to 3.024f, + Pair(5f, 2.2f) to 3.168f, + Pair(5f, 2.3f) to 3.312f, + Pair(5f, 2.4f) to 3.456f, + Pair(5f, 2.5f) to 3.600f, + Pair(5f, 2.6f) to 3.744f, + Pair(5f, 2.7f) to 3.888f, + Pair(5f, 2.8f) to 4.032f, + Pair(5f, 2.9f) to 4.176f, + Pair(5f, 3.0f) to 4.320f, + Pair(5f, 3.2f) to 4.608f, + Pair(5f, 3.4f) to 4.896f, + Pair(5f, 3.6f) to 5.184f, + Pair(5f, 3.8f) to 5.472f, + Pair(5f, 4.0f) to 5.760f, + Pair(5f, 4.2f) to 6.048f, + Pair(5f, 4.4f) to 6.336f, + Pair(5f, 4.6f) to 6.624f, + Pair(5f, 4.8f) to 6.912f, + + // Pulse Width = 10f + Pair(10f, 0.4f) to 0.576f, + Pair(10f, 0.5f) to 0.720f, + Pair(10f, 0.6f) to 0.864f, + Pair(10f, 0.7f) to 1.008f, + Pair(10f, 0.8f) to 1.152f, + Pair(10f, 0.9f) to 1.296f, + Pair(10f, 1.0f) to 1.440f, + Pair(10f, 1.1f) to 1.584f, + Pair(10f, 1.2f) to 1.728f, + Pair(10f, 1.3f) to 1.872f, + Pair(10f, 1.4f) to 2.016f, + Pair(10f, 1.5f) to 2.160f, + Pair(10f, 1.6f) to 2.304f, + Pair(10f, 1.7f) to 2.448f, + Pair(10f, 1.8f) to 2.592f, + Pair(10f, 1.9f) to 2.736f, + Pair(10f, 2.0f) to 2.880f, + Pair(10f, 2.1f) to 3.024f, + Pair(10f, 2.2f) to 3.168f, + Pair(10f, 2.3f) to 3.312f, + Pair(10f, 2.4f) to 3.456f, + Pair(10f, 2.5f) to 3.600f, + Pair(10f, 2.6f) to 3.744f, + Pair(10f, 2.7f) to 3.888f, + Pair(10f, 2.8f) to 4.032f, + Pair(10f, 2.9f) to 4.176f, + Pair(10f, 3.0f) to 4.320f, + Pair(10f, 3.2f) to 4.608f, + Pair(10f, 3.4f) to 4.896f, + Pair(10f, 3.6f) to 5.184f, + Pair(10f, 3.8f) to 5.472f, + Pair(10f, 4.0f) to 5.760f, + Pair(10f, 4.2f) to 6.048f, + Pair(10f, 4.4f) to 6.336f, + Pair(10f, 4.6f) to 6.624f, + Pair(10f, 4.8f) to 6.912f, + Pair(10f, 5.0f) to 7.200f, + Pair(10f, 5.5f) to 7.920f, + Pair(10f, 6.0f) to 8.640f, + Pair(10f, 6.5f) to 9.360f, + + // Pulse Width = 15f + Pair(15f, 0.4f) to 0.576f, + Pair(15f, 0.5f) to 0.720f, + Pair(15f, 0.6f) to 0.864f, + Pair(15f, 0.7f) to 1.008f, + Pair(15f, 0.8f) to 1.152f, + Pair(15f, 0.9f) to 1.296f, + Pair(15f, 1.0f) to 1.440f, + Pair(15f, 1.1f) to 1.584f, + Pair(15f, 1.2f) to 1.728f, + Pair(15f, 1.3f) to 1.872f, + Pair(15f, 1.4f) to 2.016f, + Pair(15f, 1.5f) to 2.160f, + Pair(15f, 1.6f) to 2.304f, + Pair(15f, 1.7f) to 2.448f, + Pair(15f, 1.8f) to 2.592f, + Pair(15f, 1.9f) to 2.736f, + Pair(15f, 2.0f) to 2.880f, + Pair(15f, 2.1f) to 3.024f, + Pair(15f, 2.2f) to 3.168f, + Pair(15f, 2.3f) to 3.312f, + Pair(15f, 2.4f) to 3.456f, + Pair(15f, 2.5f) to 3.600f, + Pair(15f, 2.6f) to 3.744f, + Pair(15f, 2.7f) to 3.888f, + Pair(15f, 2.8f) to 4.032f, + Pair(15f, 2.9f) to 4.176f, + Pair(15f, 3.0f) to 4.320f, + Pair(15f, 3.2f) to 4.608f, + Pair(15f, 3.4f) to 4.896f, + Pair(15f, 3.6f) to 5.184f, + Pair(15f, 3.8f) to 5.472f, + Pair(15f, 4.0f) to 5.760f, + Pair(15f, 4.2f) to 6.048f, + Pair(15f, 4.4f) to 6.336f, + Pair(15f, 4.6f) to 6.624f, + Pair(15f, 4.8f) to 6.912f, + Pair(15f, 5.0f) to 7.200f, + Pair(15f, 5.5f) to 7.920f, + Pair(15f, 6.0f) to 8.640f, + Pair(15f, 6.5f) to 9.360f, + + // Pulse Width = 20f + Pair(20f, 0.4f) to 0.576f, + Pair(20f, 0.5f) to 0.720f, + Pair(20f, 0.6f) to 0.864f, + Pair(20f, 0.7f) to 1.008f, + Pair(20f, 0.8f) to 1.152f, + Pair(20f, 0.9f) to 1.296f, + Pair(20f, 1.0f) to 1.440f, + Pair(20f, 1.1f) to 1.584f, + Pair(20f, 1.2f) to 1.728f, + Pair(20f, 1.3f) to 1.872f, + Pair(20f, 1.4f) to 2.016f, + Pair(20f, 1.5f) to 2.160f, + Pair(20f, 1.6f) to 2.304f, + Pair(20f, 1.7f) to 2.448f, + Pair(20f, 1.8f) to 2.592f, + Pair(20f, 1.9f) to 2.736f, + Pair(20f, 2.0f) to 2.880f, + Pair(20f, 2.1f) to 3.024f, + Pair(20f, 2.2f) to 3.168f, + Pair(20f, 2.3f) to 3.312f, + Pair(20f, 2.4f) to 3.456f, + Pair(20f, 2.5f) to 3.600f, + Pair(20f, 2.6f) to 3.744f, + Pair(20f, 2.7f) to 3.888f, + Pair(20f, 2.8f) to 4.032f, + Pair(20f, 2.9f) to 4.176f, + Pair(20f, 3.0f) to 4.320f, + Pair(20f, 3.2f) to 4.608f, + Pair(20f, 3.4f) to 4.896f, + Pair(20f, 3.6f) to 5.184f, + Pair(20f, 3.8f) to 5.472f, + Pair(20f, 4.0f) to 5.760f, + Pair(20f, 4.2f) to 6.048f, + Pair(20f, 4.4f) to 6.336f, + Pair(20f, 4.6f) to 6.624f, + Pair(20f, 4.8f) to 6.912f, + Pair(20f, 5.0f) to 7.200f, + Pair(20f, 5.5f) to 7.920f, + Pair(20f, 6.0f) to 8.640f, + Pair(20f, 6.5f) to 9.360f, + + // Pulse Width = 25f + Pair(25f, 0.4f) to 0.576f, + Pair(25f, 0.5f) to 0.720f, + Pair(25f, 0.6f) to 0.864f, + Pair(25f, 0.7f) to 1.008f, + Pair(25f, 0.8f) to 1.152f, + Pair(25f, 0.9f) to 1.296f, + Pair(25f, 1.0f) to 1.440f, + Pair(25f, 1.1f) to 1.584f, + Pair(25f, 1.2f) to 1.728f, + Pair(25f, 1.3f) to 1.872f, + Pair(25f, 1.4f) to 2.016f, + Pair(25f, 1.5f) to 2.160f, + Pair(25f, 1.6f) to 2.304f, + Pair(25f, 1.7f) to 2.448f, + Pair(25f, 1.8f) to 2.592f, + Pair(25f, 1.9f) to 2.736f, + Pair(25f, 2.0f) to 2.880f, + Pair(25f, 2.1f) to 3.024f, + Pair(25f, 2.2f) to 3.168f, + Pair(25f, 2.3f) to 3.312f, + Pair(25f, 2.4f) to 3.456f, + Pair(25f, 2.5f) to 3.600f, + Pair(25f, 2.6f) to 3.744f, + Pair(25f, 2.7f) to 3.888f, + Pair(25f, 2.8f) to 4.032f, + Pair(25f, 2.9f) to 4.176f, + Pair(25f, 3.0f) to 4.320f, + Pair(25f, 3.2f) to 4.608f, + Pair(25f, 3.4f) to 4.896f, + Pair(25f, 3.6f) to 5.184f, + Pair(25f, 3.8f) to 5.472f, + Pair(25f, 4.0f) to 5.760f, + Pair(25f, 4.2f) to 6.048f, + Pair(25f, 4.4f) to 6.336f, + Pair(25f, 4.6f) to 6.624f, + Pair(25f, 4.8f) to 6.912f, + Pair(25f, 5.0f) to 7.200f, + Pair(25f, 5.5f) to 7.920f, + Pair(25f, 6.0f) to 8.640f, + Pair(25f, 6.5f) to 9.360f, + + // Pulse Width = 30f + Pair(30f, 0.4f) to 0.576f, + Pair(30f, 0.5f) to 0.720f, + Pair(30f, 0.6f) to 0.864f, + Pair(30f, 0.7f) to 1.008f, + Pair(30f, 0.8f) to 1.152f, + Pair(30f, 0.9f) to 1.296f, + Pair(30f, 1.0f) to 1.440f, + Pair(30f, 1.1f) to 1.584f, + Pair(30f, 1.2f) to 1.728f, + Pair(30f, 1.3f) to 1.872f, + Pair(30f, 1.4f) to 2.016f, + Pair(30f, 1.5f) to 2.160f, + Pair(30f, 1.6f) to 2.304f, + Pair(30f, 1.7f) to 2.448f, + Pair(30f, 1.8f) to 2.592f, + Pair(30f, 1.9f) to 2.736f, + Pair(30f, 2.0f) to 2.880f, + Pair(30f, 2.1f) to 3.024f, + Pair(30f, 2.2f) to 3.168f, + Pair(30f, 2.3f) to 3.312f, + Pair(30f, 2.4f) to 3.456f, + Pair(30f, 2.5f) to 3.600f, + Pair(30f, 2.6f) to 3.744f, + Pair(30f, 2.7f) to 3.888f, + Pair(30f, 2.8f) to 4.032f, + Pair(30f, 2.9f) to 4.176f, + Pair(30f, 3.0f) to 4.320f, + Pair(30f, 3.2f) to 4.608f, + Pair(30f, 3.4f) to 4.896f, + Pair(30f, 3.6f) to 5.184f, + Pair(30f, 3.8f) to 5.472f, + Pair(30f, 4.0f) to 5.760f, + Pair(30f, 4.2f) to 6.048f, + Pair(30f, 4.4f) to 6.336f, + Pair(30f, 4.6f) to 6.624f, + Pair(30f, 4.8f) to 6.912f, + Pair(30f, 5.0f) to 7.200f, + Pair(30f, 5.5f) to 7.920f, + Pair(30f, 6.0f) to 8.640f, + Pair(30f, 6.5f) to 9.360f, + + // Pulse Width = 35f + Pair(35f, 0.4f) to 0.576f, + Pair(35f, 0.5f) to 0.720f, + Pair(35f, 0.6f) to 0.864f, + Pair(35f, 0.7f) to 1.008f, + Pair(35f, 0.8f) to 1.152f, + Pair(35f, 0.9f) to 1.296f, + Pair(35f, 1.0f) to 1.440f, + Pair(35f, 1.1f) to 1.584f, + Pair(35f, 1.2f) to 1.728f, + Pair(35f, 1.3f) to 1.872f, + Pair(35f, 1.4f) to 2.016f, + Pair(35f, 1.5f) to 2.160f, + Pair(35f, 1.6f) to 2.304f, + Pair(35f, 1.7f) to 2.448f, + Pair(35f, 1.8f) to 2.592f, + Pair(35f, 1.9f) to 2.736f, + Pair(35f, 2.0f) to 2.880f, + Pair(35f, 2.1f) to 3.024f, + Pair(35f, 2.2f) to 3.168f, + Pair(35f, 2.3f) to 3.312f, + Pair(35f, 2.4f) to 3.456f, + Pair(35f, 2.5f) to 3.600f, + Pair(35f, 2.6f) to 3.744f, + Pair(35f, 2.7f) to 3.888f, + Pair(35f, 2.8f) to 4.032f, + Pair(35f, 2.9f) to 4.176f, + Pair(35f, 3.0f) to 4.320f, + Pair(35f, 3.2f) to 4.608f, + Pair(35f, 3.4f) to 4.896f, + Pair(35f, 3.6f) to 5.184f, + Pair(35f, 3.8f) to 5.472f, + Pair(35f, 4.0f) to 5.760f, + Pair(35f, 4.2f) to 6.048f, + Pair(35f, 4.4f) to 6.336f, + Pair(35f, 4.6f) to 6.624f, + Pair(35f, 4.8f) to 6.912f, + Pair(35f, 5.0f) to 7.200f, + Pair(35f, 5.5f) to 7.920f, + Pair(35f, 6.0f) to 8.640f, + Pair(35f, 6.5f) to 9.360f, + + // Pulse Width = 40f + Pair(40f, 0.4f) to 0.576f, + Pair(40f, 0.5f) to 0.720f, + Pair(40f, 0.6f) to 0.864f, + Pair(40f, 0.7f) to 1.008f, + Pair(40f, 0.8f) to 1.152f, + Pair(40f, 0.9f) to 1.296f, + Pair(40f, 1.0f) to 1.440f, + Pair(40f, 1.1f) to 1.584f, + Pair(40f, 1.2f) to 1.728f, + Pair(40f, 1.3f) to 1.872f, + Pair(40f, 1.4f) to 2.016f, + Pair(40f, 1.5f) to 2.160f, + Pair(40f, 1.6f) to 2.304f, + Pair(40f, 1.7f) to 2.448f, + Pair(40f, 1.8f) to 2.592f, + Pair(40f, 1.9f) to 2.736f, + Pair(40f, 2.0f) to 2.880f, + Pair(40f, 2.1f) to 3.024f, + Pair(40f, 2.2f) to 3.168f, + Pair(40f, 2.3f) to 3.312f, + Pair(40f, 2.4f) to 3.456f, + Pair(40f, 2.5f) to 3.600f, + Pair(40f, 2.6f) to 3.744f, + Pair(40f, 2.7f) to 3.888f, + Pair(40f, 2.8f) to 4.032f, + Pair(40f, 2.9f) to 4.176f, + Pair(40f, 3.0f) to 4.320f, + Pair(40f, 3.2f) to 4.608f, + Pair(40f, 3.4f) to 4.896f, + Pair(40f, 3.6f) to 5.184f, + Pair(40f, 3.8f) to 5.472f, + Pair(40f, 4.0f) to 5.760f, + Pair(40f, 4.2f) to 6.048f, + Pair(40f, 4.4f) to 6.336f, + Pair(40f, 4.6f) to 6.624f, + Pair(40f, 4.8f) to 6.912f, + Pair(40f, 5.0f) to 7.200f, + Pair(40f, 5.5f) to 7.920f, + Pair(40f, 6.0f) to 8.640f, + Pair(40f, 6.5f) to 9.360f, + +) + diff --git a/app/src/main/java/com/laseroptek/raman/const/EnergyTable_3_15.kt b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_3_15.kt new file mode 100644 index 0000000..cfe6673 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_3_15.kt @@ -0,0 +1,572 @@ +package com.laseroptek.raman.const + +// +// Energy Table5 (HANDPIENCE-3x15) +// - Map, Float> +// - Map, VALUE> +// - Map, Energy(J)> +// + +val EnergyTable_3_15 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 1.2f) to 0.540f, + Pair(0.5f, 1.3f) to 0.585f, + Pair(0.5f, 1.4f) to 0.630f, + Pair(0.5f, 1.5f) to 0.675f, + Pair(0.5f, 1.6f) to 0.720f, + Pair(0.5f, 1.7f) to 0.765f, + Pair(0.5f, 1.8f) to 0.810f, + Pair(0.5f, 1.9f) to 0.855f, + Pair(0.5f, 2.0f) to 0.900f, + Pair(0.5f, 2.1f) to 0.945f, + Pair(0.5f, 2.2f) to 0.990f, + Pair(0.5f, 2.3f) to 1.035f, + Pair(0.5f, 2.4f) to 1.080f, + Pair(0.5f, 2.5f) to 1.125f, + Pair(0.5f, 2.6f) to 1.170f, + Pair(0.5f, 2.7f) to 1.215f, + Pair(0.5f, 2.8f) to 1.260f, + Pair(0.5f, 2.9f) to 1.305f, + Pair(0.5f, 3.0f) to 1.350f, + Pair(0.5f, 3.2f) to 1.440f, + Pair(0.5f, 3.4f) to 1.530f, + Pair(0.5f, 3.6f) to 1.620f, + Pair(0.5f, 3.8f) to 1.710f, + Pair(0.5f, 4.0f) to 1.800f, + Pair(0.5f, 4.2f) to 1.890f, + Pair(0.5f, 4.4f) to 1.980f, + + // Pulse Width = 1f + Pair(1f, 1.2f) to 0.540f, + Pair(1f, 1.3f) to 0.585f, + Pair(1f, 1.4f) to 0.630f, + Pair(1f, 1.5f) to 0.675f, + Pair(1f, 1.6f) to 0.720f, + Pair(1f, 1.7f) to 0.765f, + Pair(1f, 1.8f) to 0.810f, + Pair(1f, 1.9f) to 0.855f, + Pair(1f, 2.0f) to 0.900f, + Pair(1f, 2.1f) to 0.945f, + Pair(1f, 2.2f) to 0.990f, + Pair(1f, 2.3f) to 1.035f, + Pair(1f, 2.4f) to 1.080f, + Pair(1f, 2.5f) to 1.125f, + Pair(1f, 2.6f) to 1.170f, + Pair(1f, 2.7f) to 1.215f, + Pair(1f, 2.8f) to 1.260f, + Pair(1f, 2.9f) to 1.305f, + Pair(1f, 3.0f) to 1.350f, + Pair(1f, 3.2f) to 1.440f, + Pair(1f, 3.4f) to 1.530f, + Pair(1f, 3.6f) to 1.620f, + Pair(1f, 3.8f) to 1.710f, + Pair(1f, 4.0f) to 1.800f, + Pair(1f, 4.2f) to 1.890f, + Pair(1f, 4.4f) to 1.980f, + Pair(1f, 4.6f) to 2.070f, + Pair(1f, 4.8f) to 2.160f, + Pair(1f, 5.0f) to 2.250f, + Pair(1f, 5.5f) to 2.475f, + Pair(1f, 6.0f) to 2.700f, + Pair(1f, 6.5f) to 2.925f, + + // Pulse Width = 1.5f + Pair(1.5f, 1.2f) to 0.540f, + Pair(1.5f, 1.3f) to 0.585f, + Pair(1.5f, 1.4f) to 0.630f, + Pair(1.5f, 1.5f) to 0.675f, + Pair(1.5f, 1.6f) to 0.720f, + Pair(1.5f, 1.7f) to 0.765f, + Pair(1.5f, 1.8f) to 0.810f, + Pair(1.5f, 1.9f) to 0.855f, + Pair(1.5f, 2.0f) to 0.900f, + Pair(1.5f, 2.1f) to 0.945f, + Pair(1.5f, 2.2f) to 0.990f, + Pair(1.5f, 2.3f) to 1.035f, + Pair(1.5f, 2.4f) to 1.080f, + Pair(1.5f, 2.5f) to 1.125f, + Pair(1.5f, 2.6f) to 1.170f, + Pair(1.5f, 2.7f) to 1.215f, + Pair(1.5f, 2.8f) to 1.260f, + Pair(1.5f, 2.9f) to 1.305f, + Pair(1.5f, 3.0f) to 1.350f, + Pair(1.5f, 3.2f) to 1.440f, + Pair(1.5f, 3.4f) to 1.530f, + Pair(1.5f, 3.6f) to 1.620f, + Pair(1.5f, 3.8f) to 1.710f, + Pair(1.5f, 4.0f) to 1.800f, + Pair(1.5f, 4.2f) to 1.890f, + Pair(1.5f, 4.4f) to 1.980f, + Pair(1.5f, 4.6f) to 2.070f, + Pair(1.5f, 4.8f) to 2.160f, + Pair(1.5f, 5.0f) to 2.250f, + Pair(1.5f, 5.5f) to 2.475f, + Pair(1.5f, 6.0f) to 2.700f, + Pair(1.5f, 6.5f) to 2.925f, + Pair(1.5f, 7.0f) to 3.150f, + Pair(1.5f, 7.5f) to 3.375f, + Pair(1.5f, 8.0f) to 3.600f, + Pair(1.5f, 8.5f) to 3.825f, + + // Pulse Width = 3f + Pair(3f, 1.2f) to 0.540f, + Pair(3f, 1.3f) to 0.585f, + Pair(3f, 1.4f) to 0.630f, + Pair(3f, 1.5f) to 0.675f, + Pair(3f, 1.6f) to 0.720f, + Pair(3f, 1.7f) to 0.765f, + Pair(3f, 1.8f) to 0.810f, + Pair(3f, 1.9f) to 0.855f, + Pair(3f, 2.0f) to 0.900f, + Pair(3f, 2.1f) to 0.945f, + Pair(3f, 2.2f) to 0.990f, + Pair(3f, 2.3f) to 1.035f, + Pair(3f, 2.4f) to 1.080f, + Pair(3f, 2.5f) to 1.125f, + Pair(3f, 2.6f) to 1.170f, + Pair(3f, 2.7f) to 1.215f, + Pair(3f, 2.8f) to 1.260f, + Pair(3f, 2.9f) to 1.305f, + Pair(3f, 3.0f) to 1.350f, + Pair(3f, 3.2f) to 1.440f, + Pair(3f, 3.4f) to 1.530f, + Pair(3f, 3.6f) to 1.620f, + Pair(3f, 3.8f) to 1.710f, + Pair(3f, 4.0f) to 1.800f, + Pair(3f, 4.2f) to 1.890f, + Pair(3f, 4.4f) to 1.980f, + Pair(3f, 4.6f) to 2.070f, + Pair(3f, 4.8f) to 2.160f, + Pair(3f, 5.0f) to 2.250f, + Pair(3f, 5.5f) to 2.475f, + Pair(3f, 6.0f) to 2.700f, + Pair(3f, 6.5f) to 2.925f, + Pair(3f, 7.0f) to 3.150f, + Pair(3f, 7.5f) to 3.375f, + Pair(3f, 8.0f) to 3.600f, + Pair(3f, 8.5f) to 3.825f, + Pair(3f, 9.0f) to 4.050f, + Pair(3f, 9.5f) to 4.275f, + Pair(3f, 10.0f) to 4.500f, + Pair(3f, 11.0f) to 4.950f, + Pair(3f, 12.0f) to 5.400f, + Pair(3f, 13.0f) to 5.850f, + + // Pulse Width = 5f + Pair(5f, 1.2f) to 0.540f, + Pair(5f, 1.3f) to 0.585f, + Pair(5f, 1.4f) to 0.630f, + Pair(5f, 1.5f) to 0.675f, + Pair(5f, 1.6f) to 0.720f, + Pair(5f, 1.7f) to 0.765f, + Pair(5f, 1.8f) to 0.810f, + Pair(5f, 1.9f) to 0.855f, + Pair(5f, 2.0f) to 0.900f, + Pair(5f, 2.1f) to 0.945f, + Pair(5f, 2.2f) to 0.990f, + Pair(5f, 2.3f) to 1.035f, + Pair(5f, 2.4f) to 1.080f, + Pair(5f, 2.5f) to 1.125f, + Pair(5f, 2.6f) to 1.170f, + Pair(5f, 2.7f) to 1.215f, + Pair(5f, 2.8f) to 1.260f, + Pair(5f, 2.9f) to 1.305f, + Pair(5f, 3.0f) to 1.350f, + Pair(5f, 3.2f) to 1.440f, + Pair(5f, 3.4f) to 1.530f, + Pair(5f, 3.6f) to 1.620f, + Pair(5f, 3.8f) to 1.710f, + Pair(5f, 4.0f) to 1.800f, + Pair(5f, 4.2f) to 1.890f, + Pair(5f, 4.4f) to 1.980f, + Pair(5f, 4.6f) to 2.070f, + Pair(5f, 4.8f) to 2.160f, + Pair(5f, 5.0f) to 2.250f, + Pair(5f, 5.5f) to 2.475f, + Pair(5f, 6.0f) to 2.700f, + Pair(5f, 6.5f) to 2.925f, + Pair(5f, 7.0f) to 3.150f, + Pair(5f, 7.5f) to 3.375f, + Pair(5f, 8.0f) to 3.600f, + Pair(5f, 8.5f) to 3.825f, + Pair(5f, 9.0f) to 4.050f, + Pair(5f, 9.5f) to 4.275f, + Pair(5f, 10.0f) to 4.500f, + Pair(5f, 11.0f) to 4.950f, + Pair(5f, 12.0f) to 5.400f, + Pair(5f, 13.0f) to 5.850f, + Pair(5f, 14.0f) to 6.300f, + Pair(5f, 15.0f) to 6.750f, + + // Pulse Width = 10f + Pair(10f, 1.2f) to 0.540f, + Pair(10f, 1.3f) to 0.585f, + Pair(10f, 1.4f) to 0.630f, + Pair(10f, 1.5f) to 0.675f, + Pair(10f, 1.6f) to 0.720f, + Pair(10f, 1.7f) to 0.765f, + Pair(10f, 1.8f) to 0.810f, + Pair(10f, 1.9f) to 0.855f, + Pair(10f, 2.0f) to 0.900f, + Pair(10f, 2.1f) to 0.945f, + Pair(10f, 2.2f) to 0.990f, + Pair(10f, 2.3f) to 1.035f, + Pair(10f, 2.4f) to 1.080f, + Pair(10f, 2.5f) to 1.125f, + Pair(10f, 2.6f) to 1.170f, + Pair(10f, 2.7f) to 1.215f, + Pair(10f, 2.8f) to 1.260f, + Pair(10f, 2.9f) to 1.305f, + Pair(10f, 3.0f) to 1.350f, + Pair(10f, 3.2f) to 1.440f, + Pair(10f, 3.4f) to 1.530f, + Pair(10f, 3.6f) to 1.620f, + Pair(10f, 3.8f) to 1.710f, + Pair(10f, 4.0f) to 1.800f, + Pair(10f, 4.2f) to 1.890f, + Pair(10f, 4.4f) to 1.980f, + Pair(10f, 4.6f) to 2.070f, + Pair(10f, 4.8f) to 2.160f, + Pair(10f, 5.0f) to 2.250f, + Pair(10f, 5.5f) to 2.475f, + Pair(10f, 6.0f) to 2.700f, + Pair(10f, 6.5f) to 2.925f, + Pair(10f, 7.0f) to 3.150f, + Pair(10f, 7.5f) to 3.375f, + Pair(10f, 8.0f) to 3.600f, + Pair(10f, 8.5f) to 3.825f, + Pair(10f, 9.0f) to 4.050f, + Pair(10f, 9.5f) to 4.275f, + Pair(10f, 10.0f) to 4.500f, + Pair(10f, 11.0f) to 4.950f, + Pair(10f, 12.0f) to 5.400f, + Pair(10f, 13.0f) to 5.850f, + Pair(10f, 14.0f) to 6.300f, + Pair(10f, 15.0f) to 6.750f, + Pair(10f, 16.0f) to 7.200f, + Pair(10f, 17.0f) to 7.650f, + Pair(10f, 18.0f) to 8.100f, + Pair(10f, 19.0f) to 8.550f, + Pair(10f, 20.0f) to 9.000f, + Pair(10f, 21.0f) to 9.450f, + Pair(10f, 22.0f) to 9.900f, + + // Pulse Width = 15f + Pair(15f, 1.2f) to 0.540f, + Pair(15f, 1.3f) to 0.585f, + Pair(15f, 1.4f) to 0.630f, + Pair(15f, 1.5f) to 0.675f, + Pair(15f, 1.6f) to 0.720f, + Pair(15f, 1.7f) to 0.765f, + Pair(15f, 1.8f) to 0.810f, + Pair(15f, 1.9f) to 0.855f, + Pair(15f, 2.0f) to 0.900f, + Pair(15f, 2.1f) to 0.945f, + Pair(15f, 2.2f) to 0.990f, + Pair(15f, 2.3f) to 1.035f, + Pair(15f, 2.4f) to 1.080f, + Pair(15f, 2.5f) to 1.125f, + Pair(15f, 2.6f) to 1.170f, + Pair(15f, 2.7f) to 1.215f, + Pair(15f, 2.8f) to 1.260f, + Pair(15f, 2.9f) to 1.305f, + Pair(15f, 3.0f) to 1.350f, + Pair(15f, 3.2f) to 1.440f, + Pair(15f, 3.4f) to 1.530f, + Pair(15f, 3.6f) to 1.620f, + Pair(15f, 3.8f) to 1.710f, + Pair(15f, 4.0f) to 1.800f, + Pair(15f, 4.2f) to 1.890f, + Pair(15f, 4.4f) to 1.980f, + Pair(15f, 4.6f) to 2.070f, + Pair(15f, 4.8f) to 2.160f, + Pair(15f, 5.0f) to 2.250f, + Pair(15f, 5.5f) to 2.475f, + Pair(15f, 6.0f) to 2.700f, + Pair(15f, 6.5f) to 2.925f, + Pair(15f, 7.0f) to 3.150f, + Pair(15f, 7.5f) to 3.375f, + Pair(15f, 8.0f) to 3.600f, + Pair(15f, 8.5f) to 3.825f, + Pair(15f, 9.0f) to 4.050f, + Pair(15f, 9.5f) to 4.275f, + Pair(15f, 10.0f) to 4.500f, + Pair(15f, 11.0f) to 4.950f, + Pair(15f, 12.0f) to 5.400f, + Pair(15f, 13.0f) to 5.850f, + Pair(15f, 14.0f) to 6.300f, + Pair(15f, 15.0f) to 6.750f, + Pair(15f, 16.0f) to 7.200f, + Pair(15f, 17.0f) to 7.650f, + Pair(15f, 18.0f) to 8.100f, + Pair(15f, 19.0f) to 8.550f, + Pair(15f, 20.0f) to 9.000f, + Pair(15f, 21.0f) to 9.450f, + Pair(15f, 22.0f) to 9.900f, + + // Pulse Width = 20f + Pair(20f, 1.2f) to 0.540f, + Pair(20f, 1.3f) to 0.585f, + Pair(20f, 1.4f) to 0.630f, + Pair(20f, 1.5f) to 0.675f, + Pair(20f, 1.6f) to 0.720f, + Pair(20f, 1.7f) to 0.765f, + Pair(20f, 1.8f) to 0.810f, + Pair(20f, 1.9f) to 0.855f, + Pair(20f, 2.0f) to 0.900f, + Pair(20f, 2.1f) to 0.945f, + Pair(20f, 2.2f) to 0.990f, + Pair(20f, 2.3f) to 1.035f, + Pair(20f, 2.4f) to 1.080f, + Pair(20f, 2.5f) to 1.125f, + Pair(20f, 2.6f) to 1.170f, + Pair(20f, 2.7f) to 1.215f, + Pair(20f, 2.8f) to 1.260f, + Pair(20f, 2.9f) to 1.305f, + Pair(20f, 3.0f) to 1.350f, + Pair(20f, 3.2f) to 1.440f, + Pair(20f, 3.4f) to 1.530f, + Pair(20f, 3.6f) to 1.620f, + Pair(20f, 3.8f) to 1.710f, + Pair(20f, 4.0f) to 1.800f, + Pair(20f, 4.2f) to 1.890f, + Pair(20f, 4.4f) to 1.980f, + Pair(20f, 4.6f) to 2.070f, + Pair(20f, 4.8f) to 2.160f, + Pair(20f, 5.0f) to 2.250f, + Pair(20f, 5.5f) to 2.475f, + Pair(20f, 6.0f) to 2.700f, + Pair(20f, 6.5f) to 2.925f, + Pair(20f, 7.0f) to 3.150f, + Pair(20f, 7.5f) to 3.375f, + Pair(20f, 8.0f) to 3.600f, + Pair(20f, 8.5f) to 3.825f, + Pair(20f, 9.0f) to 4.050f, + Pair(20f, 9.5f) to 4.275f, + Pair(20f, 10.0f) to 4.500f, + Pair(20f, 11.0f) to 4.950f, + Pair(20f, 12.0f) to 5.400f, + Pair(20f, 13.0f) to 5.850f, + Pair(20f, 14.0f) to 6.300f, + Pair(20f, 15.0f) to 6.750f, + Pair(20f, 16.0f) to 7.200f, + Pair(20f, 17.0f) to 7.650f, + Pair(20f, 18.0f) to 8.100f, + Pair(20f, 19.0f) to 8.550f, + Pair(20f, 20.0f) to 9.000f, + Pair(20f, 21.0f) to 9.450f, + Pair(20f, 22.0f) to 9.900f, + + // Pulse Width = 25f + Pair(25f, 1.2f) to 0.540f, + Pair(25f, 1.3f) to 0.585f, + Pair(25f, 1.4f) to 0.630f, + Pair(25f, 1.5f) to 0.675f, + Pair(25f, 1.6f) to 0.720f, + Pair(25f, 1.7f) to 0.765f, + Pair(25f, 1.8f) to 0.810f, + Pair(25f, 1.9f) to 0.855f, + Pair(25f, 2.0f) to 0.900f, + Pair(25f, 2.1f) to 0.945f, + Pair(25f, 2.2f) to 0.990f, + Pair(25f, 2.3f) to 1.035f, + Pair(25f, 2.4f) to 1.080f, + Pair(25f, 2.5f) to 1.125f, + Pair(25f, 2.6f) to 1.170f, + Pair(25f, 2.7f) to 1.215f, + Pair(25f, 2.8f) to 1.260f, + Pair(25f, 2.9f) to 1.305f, + Pair(25f, 3.0f) to 1.350f, + Pair(25f, 3.2f) to 1.440f, + Pair(25f, 3.4f) to 1.530f, + Pair(25f, 3.6f) to 1.620f, + Pair(25f, 3.8f) to 1.710f, + Pair(25f, 4.0f) to 1.800f, + Pair(25f, 4.2f) to 1.890f, + Pair(25f, 4.4f) to 1.980f, + Pair(25f, 4.6f) to 2.070f, + Pair(25f, 4.8f) to 2.160f, + Pair(25f, 5.0f) to 2.250f, + Pair(25f, 5.5f) to 2.475f, + Pair(25f, 6.0f) to 2.700f, + Pair(25f, 6.5f) to 2.925f, + Pair(25f, 7.0f) to 3.150f, + Pair(25f, 7.5f) to 3.375f, + Pair(25f, 8.0f) to 3.600f, + Pair(25f, 8.5f) to 3.825f, + Pair(25f, 9.0f) to 4.050f, + Pair(25f, 9.5f) to 4.275f, + Pair(25f, 10.0f) to 4.500f, + Pair(25f, 11.0f) to 4.950f, + Pair(25f, 12.0f) to 5.400f, + Pair(25f, 13.0f) to 5.850f, + Pair(25f, 14.0f) to 6.300f, + Pair(25f, 15.0f) to 6.750f, + Pair(25f, 16.0f) to 7.200f, + Pair(25f, 17.0f) to 7.650f, + Pair(25f, 18.0f) to 8.100f, + Pair(25f, 19.0f) to 8.550f, + Pair(25f, 20.0f) to 9.000f, + Pair(25f, 21.0f) to 9.450f, + Pair(25f, 22.0f) to 9.900f, + + // Pulse Width = 30f + Pair(30f, 1.2f) to 0.540f, + Pair(30f, 1.3f) to 0.585f, + Pair(30f, 1.4f) to 0.630f, + Pair(30f, 1.5f) to 0.675f, + Pair(30f, 1.6f) to 0.720f, + Pair(30f, 1.7f) to 0.765f, + Pair(30f, 1.8f) to 0.810f, + Pair(30f, 1.9f) to 0.855f, + Pair(30f, 2.0f) to 0.900f, + Pair(30f, 2.1f) to 0.945f, + Pair(30f, 2.2f) to 0.990f, + Pair(30f, 2.3f) to 1.035f, + Pair(30f, 2.4f) to 1.080f, + Pair(30f, 2.5f) to 1.125f, + Pair(30f, 2.6f) to 1.170f, + Pair(30f, 2.7f) to 1.215f, + Pair(30f, 2.8f) to 1.260f, + Pair(30f, 2.9f) to 1.305f, + Pair(30f, 3.0f) to 1.350f, + Pair(30f, 3.2f) to 1.440f, + Pair(30f, 3.4f) to 1.530f, + Pair(30f, 3.6f) to 1.620f, + Pair(30f, 3.8f) to 1.710f, + Pair(30f, 4.0f) to 1.800f, + Pair(30f, 4.2f) to 1.890f, + Pair(30f, 4.4f) to 1.980f, + Pair(30f, 4.6f) to 2.070f, + Pair(30f, 4.8f) to 2.160f, + Pair(30f, 5.0f) to 2.250f, + Pair(30f, 5.5f) to 2.475f, + Pair(30f, 6.0f) to 2.700f, + Pair(30f, 6.5f) to 2.925f, + Pair(30f, 7.0f) to 3.150f, + Pair(30f, 7.5f) to 3.375f, + Pair(30f, 8.0f) to 3.600f, + Pair(30f, 8.5f) to 3.825f, + Pair(30f, 9.0f) to 4.050f, + Pair(30f, 9.5f) to 4.275f, + Pair(30f, 10.0f) to 4.500f, + Pair(30f, 11.0f) to 4.950f, + Pair(30f, 12.0f) to 5.400f, + Pair(30f, 13.0f) to 5.850f, + Pair(30f, 14.0f) to 6.300f, + Pair(30f, 15.0f) to 6.750f, + Pair(30f, 16.0f) to 7.200f, + Pair(30f, 17.0f) to 7.650f, + Pair(30f, 18.0f) to 8.100f, + Pair(30f, 19.0f) to 8.550f, + Pair(30f, 20.0f) to 9.000f, + Pair(30f, 21.0f) to 9.450f, + Pair(30f, 22.0f) to 9.900f, + + // Pulse Width = 35f + Pair(35f, 1.2f) to 0.540f, + Pair(35f, 1.3f) to 0.585f, + Pair(35f, 1.4f) to 0.630f, + Pair(35f, 1.5f) to 0.675f, + Pair(35f, 1.6f) to 0.720f, + Pair(35f, 1.7f) to 0.765f, + Pair(35f, 1.8f) to 0.810f, + Pair(35f, 1.9f) to 0.855f, + Pair(35f, 2.0f) to 0.900f, + Pair(35f, 2.1f) to 0.945f, + Pair(35f, 2.2f) to 0.990f, + Pair(35f, 2.3f) to 1.035f, + Pair(35f, 2.4f) to 1.080f, + Pair(35f, 2.5f) to 1.125f, + Pair(35f, 2.6f) to 1.170f, + Pair(35f, 2.7f) to 1.215f, + Pair(35f, 2.8f) to 1.260f, + Pair(35f, 2.9f) to 1.305f, + Pair(35f, 3.0f) to 1.350f, + Pair(35f, 3.2f) to 1.440f, + Pair(35f, 3.4f) to 1.530f, + Pair(35f, 3.6f) to 1.620f, + Pair(35f, 3.8f) to 1.710f, + Pair(35f, 4.0f) to 1.800f, + Pair(35f, 4.2f) to 1.890f, + Pair(35f, 4.4f) to 1.980f, + Pair(35f, 4.6f) to 2.070f, + Pair(35f, 4.8f) to 2.160f, + Pair(35f, 5.0f) to 2.250f, + Pair(35f, 5.5f) to 2.475f, + Pair(35f, 6.0f) to 2.700f, + Pair(35f, 6.5f) to 2.925f, + Pair(35f, 7.0f) to 3.150f, + Pair(35f, 7.5f) to 3.375f, + Pair(35f, 8.0f) to 3.600f, + Pair(35f, 8.5f) to 3.825f, + Pair(35f, 9.0f) to 4.050f, + Pair(35f, 9.5f) to 4.275f, + Pair(35f, 10.0f) to 4.500f, + Pair(35f, 11.0f) to 4.950f, + Pair(35f, 12.0f) to 5.400f, + Pair(35f, 13.0f) to 5.850f, + Pair(35f, 14.0f) to 6.300f, + Pair(35f, 15.0f) to 6.750f, + Pair(35f, 16.0f) to 7.200f, + Pair(35f, 17.0f) to 7.650f, + Pair(35f, 18.0f) to 8.100f, + Pair(35f, 19.0f) to 8.550f, + Pair(35f, 20.0f) to 9.000f, + Pair(35f, 21.0f) to 9.450f, + Pair(35f, 22.0f) to 9.900f, + + // Pulse Width = 40f + Pair(40f, 1.2f) to 0.540f, + Pair(40f, 1.3f) to 0.585f, + Pair(40f, 1.4f) to 0.630f, + Pair(40f, 1.5f) to 0.675f, + Pair(40f, 1.6f) to 0.720f, + Pair(40f, 1.7f) to 0.765f, + Pair(40f, 1.8f) to 0.810f, + Pair(40f, 1.9f) to 0.855f, + Pair(40f, 2.0f) to 0.900f, + Pair(40f, 2.1f) to 0.945f, + Pair(40f, 2.2f) to 0.990f, + Pair(40f, 2.3f) to 1.035f, + Pair(40f, 2.4f) to 1.080f, + Pair(40f, 2.5f) to 1.125f, + Pair(40f, 2.6f) to 1.170f, + Pair(40f, 2.7f) to 1.215f, + Pair(40f, 2.8f) to 1.260f, + Pair(40f, 2.9f) to 1.305f, + Pair(40f, 3.0f) to 1.350f, + Pair(40f, 3.2f) to 1.440f, + Pair(40f, 3.4f) to 1.530f, + Pair(40f, 3.6f) to 1.620f, + Pair(40f, 3.8f) to 1.710f, + Pair(40f, 4.0f) to 1.800f, + Pair(40f, 4.2f) to 1.890f, + Pair(40f, 4.4f) to 1.980f, + Pair(40f, 4.6f) to 2.070f, + Pair(40f, 4.8f) to 2.160f, + Pair(40f, 5.0f) to 2.250f, + Pair(40f, 5.5f) to 2.475f, + Pair(40f, 6.0f) to 2.700f, + Pair(40f, 6.5f) to 2.925f, + Pair(40f, 7.0f) to 3.150f, + Pair(40f, 7.5f) to 3.375f, + Pair(40f, 8.0f) to 3.600f, + Pair(40f, 8.5f) to 3.825f, + Pair(40f, 9.0f) to 4.050f, + Pair(40f, 9.5f) to 4.275f, + Pair(40f, 10.0f) to 4.500f, + Pair(40f, 11.0f) to 4.950f, + Pair(40f, 12.0f) to 5.400f, + Pair(40f, 13.0f) to 5.850f, + Pair(40f, 14.0f) to 6.300f, + Pair(40f, 15.0f) to 6.750f, + Pair(40f, 16.0f) to 7.200f, + Pair(40f, 17.0f) to 7.650f, + Pair(40f, 18.0f) to 8.100f, + Pair(40f, 19.0f) to 8.550f, + Pair(40f, 20.0f) to 9.000f, + Pair(40f, 21.0f) to 9.450f, + Pair(40f, 22.0f) to 9.900f, +) + diff --git a/app/src/main/java/com/laseroptek/raman/const/EnergyTable_7_7.kt b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_7_7.kt new file mode 100644 index 0000000..571a1d4 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/EnergyTable_7_7.kt @@ -0,0 +1,563 @@ +package com.laseroptek.raman.const + +// +// Energy Table2 (HANDPIENCE-7x7) +// - Map, Float> +// - Map, VALUE> +// - Map, Energy(J)> +// + +val EnergyTable_7_7 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 1.1f) to 0.539f, + Pair(0.5f, 1.2f) to 0.588f, + Pair(0.5f, 1.3f) to 0.637f, + Pair(0.5f, 1.4f) to 0.686f, + Pair(0.5f, 1.5f) to 0.735f, + Pair(0.5f, 1.6f) to 0.784f, + Pair(0.5f, 1.7f) to 0.833f, + Pair(0.5f, 1.8f) to 0.882f, + Pair(0.5f, 1.9f) to 0.931f, + Pair(0.5f, 2.0f) to 0.980f, + Pair(0.5f, 2.1f) to 1.029f, + Pair(0.5f, 2.2f) to 1.078f, + Pair(0.5f, 2.3f) to 1.127f, + Pair(0.5f, 2.4f) to 1.176f, + Pair(0.5f, 2.5f) to 1.225f, + Pair(0.5f, 2.6f) to 1.274f, + Pair(0.5f, 2.7f) to 1.323f, + Pair(0.5f, 2.8f) to 1.372f, + Pair(0.5f, 2.9f) to 1.421f, + Pair(0.5f, 3.0f) to 1.470f, + Pair(0.5f, 3.2f) to 1.568f, + Pair(0.5f, 3.4f) to 1.666f, + Pair(0.5f, 3.6f) to 1.764f, + Pair(0.5f, 3.8f) to 1.862f, + Pair(0.5f, 4.0f) to 1.960f, + + // Pulse Width = 1f + Pair(1f, 1.1f) to 0.539f, + Pair(1f, 1.2f) to 0.588f, + Pair(1f, 1.3f) to 0.637f, + Pair(1f, 1.4f) to 0.686f, + Pair(1f, 1.5f) to 0.735f, + Pair(1f, 1.6f) to 0.784f, + Pair(1f, 1.7f) to 0.833f, + Pair(1f, 1.8f) to 0.882f, + Pair(1f, 1.9f) to 0.931f, + Pair(1f, 2.0f) to 0.980f, + Pair(1f, 2.1f) to 1.029f, + Pair(1f, 2.2f) to 1.078f, + Pair(1f, 2.3f) to 1.127f, + Pair(1f, 2.4f) to 1.176f, + Pair(1f, 2.5f) to 1.225f, + Pair(1f, 2.6f) to 1.274f, + Pair(1f, 2.7f) to 1.323f, + Pair(1f, 2.8f) to 1.372f, + Pair(1f, 2.9f) to 1.421f, + Pair(1f, 3.0f) to 1.470f, + Pair(1f, 3.2f) to 1.568f, + Pair(1f, 3.4f) to 1.666f, + Pair(1f, 3.6f) to 1.764f, + Pair(1f, 3.8f) to 1.862f, + Pair(1f, 4.0f) to 1.960f, + Pair(1f, 4.2f) to 2.058f, + Pair(1f, 4.4f) to 2.156f, + Pair(1f, 4.6f) to 2.254f, + Pair(1f, 4.8f) to 2.352f, + Pair(1f, 5.0f) to 2.450f, + Pair(1f, 5.5f) to 2.695f, + Pair(1f, 6.0f) to 2.940f, + + // Pulse Width = 1.5f + Pair(1.5f, 1.1f) to 0.539f, + Pair(1.5f, 1.2f) to 0.588f, + Pair(1.5f, 1.3f) to 0.637f, + Pair(1.5f, 1.4f) to 0.686f, + Pair(1.5f, 1.5f) to 0.735f, + Pair(1.5f, 1.6f) to 0.784f, + Pair(1.5f, 1.7f) to 0.833f, + Pair(1.5f, 1.8f) to 0.882f, + Pair(1.5f, 1.9f) to 0.931f, + Pair(1.5f, 2.0f) to 0.980f, + Pair(1.5f, 2.1f) to 1.029f, + Pair(1.5f, 2.2f) to 1.078f, + Pair(1.5f, 2.3f) to 1.127f, + Pair(1.5f, 2.4f) to 1.176f, + Pair(1.5f, 2.5f) to 1.225f, + Pair(1.5f, 2.6f) to 1.274f, + Pair(1.5f, 2.7f) to 1.323f, + Pair(1.5f, 2.8f) to 1.372f, + Pair(1.5f, 2.9f) to 1.421f, + Pair(1.5f, 3.0f) to 1.470f, + Pair(1.5f, 3.2f) to 1.568f, + Pair(1.5f, 3.4f) to 1.666f, + Pair(1.5f, 3.6f) to 1.764f, + Pair(1.5f, 3.8f) to 1.862f, + Pair(1.5f, 4.0f) to 1.960f, + Pair(1.5f, 4.2f) to 2.058f, + Pair(1.5f, 4.4f) to 2.156f, + Pair(1.5f, 4.6f) to 2.254f, + Pair(1.5f, 4.8f) to 2.352f, + Pair(1.5f, 5.0f) to 2.450f, + Pair(1.5f, 5.5f) to 2.695f, + Pair(1.5f, 6.0f) to 2.940f, + Pair(1.5f, 6.5f) to 3.185f, + Pair(1.5f, 7.0f) to 3.430f, + Pair(1.5f, 7.5f) to 3.675f, + Pair(1.5f, 8.0f) to 3.920f, + + // Pulse Width = 3f + Pair(3f, 1.1f) to 0.539f, + Pair(3f, 1.2f) to 0.588f, + Pair(3f, 1.3f) to 0.637f, + Pair(3f, 1.4f) to 0.686f, + Pair(3f, 1.5f) to 0.735f, + Pair(3f, 1.6f) to 0.784f, + Pair(3f, 1.7f) to 0.833f, + Pair(3f, 1.8f) to 0.882f, + Pair(3f, 1.9f) to 0.931f, + Pair(3f, 2.0f) to 0.980f, + Pair(3f, 2.1f) to 1.029f, + Pair(3f, 2.2f) to 1.078f, + Pair(3f, 2.3f) to 1.127f, + Pair(3f, 2.4f) to 1.176f, + Pair(3f, 2.5f) to 1.225f, + Pair(3f, 2.6f) to 1.274f, + Pair(3f, 2.7f) to 1.323f, + Pair(3f, 2.8f) to 1.372f, + Pair(3f, 2.9f) to 1.421f, + Pair(3f, 3.0f) to 1.470f, + Pair(3f, 3.2f) to 1.568f, + Pair(3f, 3.4f) to 1.666f, + Pair(3f, 3.6f) to 1.764f, + Pair(3f, 3.8f) to 1.862f, + Pair(3f, 4.0f) to 1.960f, + Pair(3f, 4.2f) to 2.058f, + Pair(3f, 4.4f) to 2.156f, + Pair(3f, 4.6f) to 2.254f, + Pair(3f, 4.8f) to 2.352f, + Pair(3f, 5.0f) to 2.450f, + Pair(3f, 5.5f) to 2.695f, + Pair(3f, 6.0f) to 2.940f, + Pair(3f, 6.5f) to 3.185f, + Pair(3f, 7.0f) to 3.430f, + Pair(3f, 7.5f) to 3.675f, + Pair(3f, 8.0f) to 3.920f, + Pair(3f, 8.5f) to 4.165f, + Pair(3f, 9.0f) to 4.410f, + Pair(3f, 9.5f) to 4.655f, + Pair(3f, 10.0f) to 4.900f, + Pair(3f, 11.0f) to 5.390f, + Pair(3f, 12.0f) to 5.880f, + + // Pulse Width = 5f + Pair(5f, 1.1f) to 0.539f, + Pair(5f, 1.2f) to 0.588f, + Pair(5f, 1.3f) to 0.637f, + Pair(5f, 1.4f) to 0.686f, + Pair(5f, 1.5f) to 0.735f, + Pair(5f, 1.6f) to 0.784f, + Pair(5f, 1.7f) to 0.833f, + Pair(5f, 1.8f) to 0.882f, + Pair(5f, 1.9f) to 0.931f, + Pair(5f, 2.0f) to 0.980f, + Pair(5f, 2.1f) to 1.029f, + Pair(5f, 2.2f) to 1.078f, + Pair(5f, 2.3f) to 1.127f, + Pair(5f, 2.4f) to 1.176f, + Pair(5f, 2.5f) to 1.225f, + Pair(5f, 2.6f) to 1.274f, + Pair(5f, 2.7f) to 1.323f, + Pair(5f, 2.8f) to 1.372f, + Pair(5f, 2.9f) to 1.421f, + Pair(5f, 3.0f) to 1.470f, + Pair(5f, 3.2f) to 1.568f, + Pair(5f, 3.4f) to 1.666f, + Pair(5f, 3.6f) to 1.764f, + Pair(5f, 3.8f) to 1.862f, + Pair(5f, 4.0f) to 1.960f, + Pair(5f, 4.2f) to 2.058f, + Pair(5f, 4.4f) to 2.156f, + Pair(5f, 4.6f) to 2.254f, + Pair(5f, 4.8f) to 2.352f, + Pair(5f, 5.0f) to 2.450f, + Pair(5f, 5.5f) to 2.695f, + Pair(5f, 6.0f) to 2.940f, + Pair(5f, 6.5f) to 3.185f, + Pair(5f, 7.0f) to 3.430f, + Pair(5f, 7.5f) to 3.675f, + Pair(5f, 8.0f) to 3.920f, + Pair(5f, 8.5f) to 4.165f, + Pair(5f, 9.0f) to 4.410f, + Pair(5f, 9.5f) to 4.655f, + Pair(5f, 10.0f) to 4.900f, + Pair(5f, 11.0f) to 5.390f, + Pair(5f, 12.0f) to 5.880f, + Pair(5f, 13.0f) to 6.370f, + Pair(5f, 14.0f) to 6.860f, + + // Pulse Width = 10f + Pair(10f, 1.1f) to 0.539f, + Pair(10f, 1.2f) to 0.588f, + Pair(10f, 1.3f) to 0.637f, + Pair(10f, 1.4f) to 0.686f, + Pair(10f, 1.5f) to 0.735f, + Pair(10f, 1.6f) to 0.784f, + Pair(10f, 1.7f) to 0.833f, + Pair(10f, 1.8f) to 0.882f, + Pair(10f, 1.9f) to 0.931f, + Pair(10f, 2.0f) to 0.980f, + Pair(10f, 2.1f) to 1.029f, + Pair(10f, 2.2f) to 1.078f, + Pair(10f, 2.3f) to 1.127f, + Pair(10f, 2.4f) to 1.176f, + Pair(10f, 2.5f) to 1.225f, + Pair(10f, 2.6f) to 1.274f, + Pair(10f, 2.7f) to 1.323f, + Pair(10f, 2.8f) to 1.372f, + Pair(10f, 2.9f) to 1.421f, + Pair(10f, 3.0f) to 1.470f, + Pair(10f, 3.2f) to 1.568f, + Pair(10f, 3.4f) to 1.666f, + Pair(10f, 3.6f) to 1.764f, + Pair(10f, 3.8f) to 1.862f, + Pair(10f, 4.0f) to 1.960f, + Pair(10f, 4.2f) to 2.058f, + Pair(10f, 4.4f) to 2.156f, + Pair(10f, 4.6f) to 2.254f, + Pair(10f, 4.8f) to 2.352f, + Pair(10f, 5.0f) to 2.450f, + Pair(10f, 5.5f) to 2.695f, + Pair(10f, 6.0f) to 2.940f, + Pair(10f, 6.5f) to 3.185f, + Pair(10f, 7.0f) to 3.430f, + Pair(10f, 7.5f) to 3.675f, + Pair(10f, 8.0f) to 3.920f, + Pair(10f, 8.5f) to 4.165f, + Pair(10f, 9.0f) to 4.410f, + Pair(10f, 9.5f) to 4.655f, + Pair(10f, 10.0f) to 4.900f, + Pair(10f, 11.0f) to 5.390f, + Pair(10f, 12.0f) to 5.880f, + Pair(10f, 13.0f) to 6.370f, + Pair(10f, 14.0f) to 6.860f, + Pair(10f, 15.0f) to 7.350f, + Pair(10f, 16.0f) to 7.840f, + Pair(10f, 17.0f) to 8.330f, + Pair(10f, 18.0f) to 8.820f, + Pair(10f, 19.0f) to 9.310f, + Pair(10f, 20.0f) to 9.800f, + + // Pulse Width = 15f + Pair(15f, 1.1f) to 0.539f, + Pair(15f, 1.2f) to 0.588f, + Pair(15f, 1.3f) to 0.637f, + Pair(15f, 1.4f) to 0.686f, + Pair(15f, 1.5f) to 0.735f, + Pair(15f, 1.6f) to 0.784f, + Pair(15f, 1.7f) to 0.833f, + Pair(15f, 1.8f) to 0.882f, + Pair(15f, 1.9f) to 0.931f, + Pair(15f, 2.0f) to 0.980f, + Pair(15f, 2.1f) to 1.029f, + Pair(15f, 2.2f) to 1.078f, + Pair(15f, 2.3f) to 1.127f, + Pair(15f, 2.4f) to 1.176f, + Pair(15f, 2.5f) to 1.225f, + Pair(15f, 2.6f) to 1.274f, + Pair(15f, 2.7f) to 1.323f, + Pair(15f, 2.8f) to 1.372f, + Pair(15f, 2.9f) to 1.421f, + Pair(15f, 3.0f) to 1.470f, + Pair(15f, 3.2f) to 1.568f, + Pair(15f, 3.4f) to 1.666f, + Pair(15f, 3.6f) to 1.764f, + Pair(15f, 3.8f) to 1.862f, + Pair(15f, 4.0f) to 1.960f, + Pair(15f, 4.2f) to 2.058f, + Pair(15f, 4.4f) to 2.156f, + Pair(15f, 4.6f) to 2.254f, + Pair(15f, 4.8f) to 2.352f, + Pair(15f, 5.0f) to 2.450f, + Pair(15f, 5.5f) to 2.695f, + Pair(15f, 6.0f) to 2.940f, + Pair(15f, 6.5f) to 3.185f, + Pair(15f, 7.0f) to 3.430f, + Pair(15f, 7.5f) to 3.675f, + Pair(15f, 8.0f) to 3.920f, + Pair(15f, 8.5f) to 4.165f, + Pair(15f, 9.0f) to 4.410f, + Pair(15f, 9.5f) to 4.655f, + Pair(15f, 10.0f) to 4.900f, + Pair(15f, 11.0f) to 5.390f, + Pair(15f, 12.0f) to 5.880f, + Pair(15f, 13.0f) to 6.370f, + Pair(15f, 14.0f) to 6.860f, + Pair(15f, 15.0f) to 7.350f, + Pair(15f, 16.0f) to 7.840f, + Pair(15f, 17.0f) to 8.330f, + Pair(15f, 18.0f) to 8.820f, + Pair(15f, 19.0f) to 9.310f, + Pair(15f, 20.0f) to 9.800f, + + // Pulse Width = 20f + Pair(20f, 1.1f) to 0.539f, + Pair(20f, 1.2f) to 0.588f, + Pair(20f, 1.3f) to 0.637f, + Pair(20f, 1.4f) to 0.686f, + Pair(20f, 1.5f) to 0.735f, + Pair(20f, 1.6f) to 0.784f, + Pair(20f, 1.7f) to 0.833f, + Pair(20f, 1.8f) to 0.882f, + Pair(20f, 1.9f) to 0.931f, + Pair(20f, 2.0f) to 0.980f, + Pair(20f, 2.1f) to 1.029f, + Pair(20f, 2.2f) to 1.078f, + Pair(20f, 2.3f) to 1.127f, + Pair(20f, 2.4f) to 1.176f, + Pair(20f, 2.5f) to 1.225f, + Pair(20f, 2.6f) to 1.274f, + Pair(20f, 2.7f) to 1.323f, + Pair(20f, 2.8f) to 1.372f, + Pair(20f, 2.9f) to 1.421f, + Pair(20f, 3.0f) to 1.470f, + Pair(20f, 3.2f) to 1.568f, + Pair(20f, 3.4f) to 1.666f, + Pair(20f, 3.6f) to 1.764f, + Pair(20f, 3.8f) to 1.862f, + Pair(20f, 4.0f) to 1.960f, + Pair(20f, 4.2f) to 2.058f, + Pair(20f, 4.4f) to 2.156f, + Pair(20f, 4.6f) to 2.254f, + Pair(20f, 4.8f) to 2.352f, + Pair(20f, 5.0f) to 2.450f, + Pair(20f, 5.5f) to 2.695f, + Pair(20f, 6.0f) to 2.940f, + Pair(20f, 6.5f) to 3.185f, + Pair(20f, 7.0f) to 3.430f, + Pair(20f, 7.5f) to 3.675f, + Pair(20f, 8.0f) to 3.920f, + Pair(20f, 8.5f) to 4.165f, + Pair(20f, 9.0f) to 4.410f, + Pair(20f, 9.5f) to 4.655f, + Pair(20f, 10.0f) to 4.900f, + Pair(20f, 11.0f) to 5.390f, + Pair(20f, 12.0f) to 5.880f, + Pair(20f, 13.0f) to 6.370f, + Pair(20f, 14.0f) to 6.860f, + Pair(20f, 15.0f) to 7.350f, + Pair(20f, 16.0f) to 7.840f, + Pair(20f, 17.0f) to 8.330f, + Pair(20f, 18.0f) to 8.820f, + Pair(20f, 19.0f) to 9.310f, + Pair(20f, 20.0f) to 9.800f, + + // Pulse Width = 25f + Pair(25f, 1.1f) to 0.539f, + Pair(25f, 1.2f) to 0.588f, + Pair(25f, 1.3f) to 0.637f, + Pair(25f, 1.4f) to 0.686f, + Pair(25f, 1.5f) to 0.735f, + Pair(25f, 1.6f) to 0.784f, + Pair(25f, 1.7f) to 0.833f, + Pair(25f, 1.8f) to 0.882f, + Pair(25f, 1.9f) to 0.931f, + Pair(25f, 2.0f) to 0.980f, + Pair(25f, 2.1f) to 1.029f, + Pair(25f, 2.2f) to 1.078f, + Pair(25f, 2.3f) to 1.127f, + Pair(25f, 2.4f) to 1.176f, + Pair(25f, 2.5f) to 1.225f, + Pair(25f, 2.6f) to 1.274f, + Pair(25f, 2.7f) to 1.323f, + Pair(25f, 2.8f) to 1.372f, + Pair(25f, 2.9f) to 1.421f, + Pair(25f, 3.0f) to 1.470f, + Pair(25f, 3.2f) to 1.568f, + Pair(25f, 3.4f) to 1.666f, + Pair(25f, 3.6f) to 1.764f, + Pair(25f, 3.8f) to 1.862f, + Pair(25f, 4.0f) to 1.960f, + Pair(25f, 4.2f) to 2.058f, + Pair(25f, 4.4f) to 2.156f, + Pair(25f, 4.6f) to 2.254f, + Pair(25f, 4.8f) to 2.352f, + Pair(25f, 5.0f) to 2.450f, + Pair(25f, 5.5f) to 2.695f, + Pair(25f, 6.0f) to 2.940f, + Pair(25f, 6.5f) to 3.185f, + Pair(25f, 7.0f) to 3.430f, + Pair(25f, 7.5f) to 3.675f, + Pair(25f, 8.0f) to 3.920f, + Pair(25f, 8.5f) to 4.165f, + Pair(25f, 9.0f) to 4.410f, + Pair(25f, 9.5f) to 4.655f, + Pair(25f, 10.0f) to 4.900f, + Pair(25f, 11.0f) to 5.390f, + Pair(25f, 12.0f) to 5.880f, + Pair(25f, 13.0f) to 6.370f, + Pair(25f, 14.0f) to 6.860f, + Pair(25f, 15.0f) to 7.350f, + Pair(25f, 16.0f) to 7.840f, + Pair(25f, 17.0f) to 8.330f, + Pair(25f, 18.0f) to 8.820f, + Pair(25f, 19.0f) to 9.310f, + Pair(25f, 20.0f) to 9.800f, + + // Pulse Width = 30f + Pair(30f, 1.1f) to 0.539f, + Pair(30f, 1.2f) to 0.588f, + Pair(30f, 1.3f) to 0.637f, + Pair(30f, 1.4f) to 0.686f, + Pair(30f, 1.5f) to 0.735f, + Pair(30f, 1.6f) to 0.784f, + Pair(30f, 1.7f) to 0.833f, + Pair(30f, 1.8f) to 0.882f, + Pair(30f, 1.9f) to 0.931f, + Pair(30f, 2.0f) to 0.980f, + Pair(30f, 2.1f) to 1.029f, + Pair(30f, 2.2f) to 1.078f, + Pair(30f, 2.3f) to 1.127f, + Pair(30f, 2.4f) to 1.176f, + Pair(30f, 2.5f) to 1.225f, + Pair(30f, 2.6f) to 1.274f, + Pair(30f, 2.7f) to 1.323f, + Pair(30f, 2.8f) to 1.372f, + Pair(30f, 2.9f) to 1.421f, + Pair(30f, 3.0f) to 1.470f, + Pair(30f, 3.2f) to 1.568f, + Pair(30f, 3.4f) to 1.666f, + Pair(30f, 3.6f) to 1.764f, + Pair(30f, 3.8f) to 1.862f, + Pair(30f, 4.0f) to 1.960f, + Pair(30f, 4.2f) to 2.058f, + Pair(30f, 4.4f) to 2.156f, + Pair(30f, 4.6f) to 2.254f, + Pair(30f, 4.8f) to 2.352f, + Pair(30f, 5.0f) to 2.450f, + Pair(30f, 5.5f) to 2.695f, + Pair(30f, 6.0f) to 2.940f, + Pair(30f, 6.5f) to 3.185f, + Pair(30f, 7.0f) to 3.430f, + Pair(30f, 7.5f) to 3.675f, + Pair(30f, 8.0f) to 3.920f, + Pair(30f, 8.5f) to 4.165f, + Pair(30f, 9.0f) to 4.410f, + Pair(30f, 9.5f) to 4.655f, + Pair(30f, 10.0f) to 4.900f, + Pair(30f, 11.0f) to 5.390f, + Pair(30f, 12.0f) to 5.880f, + Pair(30f, 13.0f) to 6.370f, + Pair(30f, 14.0f) to 6.860f, + Pair(30f, 15.0f) to 7.350f, + Pair(30f, 16.0f) to 7.840f, + Pair(30f, 17.0f) to 8.330f, + Pair(30f, 18.0f) to 8.820f, + Pair(30f, 19.0f) to 9.310f, + Pair(30f, 20.0f) to 9.800f, + + // Pulse Width = 35f + Pair(35f, 1.1f) to 0.539f, + Pair(35f, 1.2f) to 0.588f, + Pair(35f, 1.3f) to 0.637f, + Pair(35f, 1.4f) to 0.686f, + Pair(35f, 1.5f) to 0.735f, + Pair(35f, 1.6f) to 0.784f, + Pair(35f, 1.7f) to 0.833f, + Pair(35f, 1.8f) to 0.882f, + Pair(35f, 1.9f) to 0.931f, + Pair(35f, 2.0f) to 0.980f, + Pair(35f, 2.1f) to 1.029f, + Pair(35f, 2.2f) to 1.078f, + Pair(35f, 2.3f) to 1.127f, + Pair(35f, 2.4f) to 1.176f, + Pair(35f, 2.5f) to 1.225f, + Pair(35f, 2.6f) to 1.274f, + Pair(35f, 2.7f) to 1.323f, + Pair(35f, 2.8f) to 1.372f, + Pair(35f, 2.9f) to 1.421f, + Pair(35f, 3.0f) to 1.470f, + Pair(35f, 3.2f) to 1.568f, + Pair(35f, 3.4f) to 1.666f, + Pair(35f, 3.6f) to 1.764f, + Pair(35f, 3.8f) to 1.862f, + Pair(35f, 4.0f) to 1.960f, + Pair(35f, 4.2f) to 2.058f, + Pair(35f, 4.4f) to 2.156f, + Pair(35f, 4.6f) to 2.254f, + Pair(35f, 4.8f) to 2.352f, + Pair(35f, 5.0f) to 2.450f, + Pair(35f, 5.5f) to 2.695f, + Pair(35f, 6.0f) to 2.940f, + Pair(35f, 6.5f) to 3.185f, + Pair(35f, 7.0f) to 3.430f, + Pair(35f, 7.5f) to 3.675f, + Pair(35f, 8.0f) to 3.920f, + Pair(35f, 8.5f) to 4.165f, + Pair(35f, 9.0f) to 4.410f, + Pair(35f, 9.5f) to 4.655f, + Pair(35f, 10.0f) to 4.900f, + Pair(35f, 11.0f) to 5.390f, + Pair(35f, 12.0f) to 5.880f, + Pair(35f, 13.0f) to 6.370f, + Pair(35f, 14.0f) to 6.860f, + Pair(35f, 15.0f) to 7.350f, + Pair(35f, 16.0f) to 7.840f, + Pair(35f, 17.0f) to 8.330f, + Pair(35f, 18.0f) to 8.820f, + Pair(35f, 19.0f) to 9.310f, + Pair(35f, 20.0f) to 9.800f, + + // Pulse Width = 40f + Pair(40f, 1.1f) to 0.539f, + Pair(40f, 1.2f) to 0.588f, + Pair(40f, 1.3f) to 0.637f, + Pair(40f, 1.4f) to 0.686f, + Pair(40f, 1.5f) to 0.735f, + Pair(40f, 1.6f) to 0.784f, + Pair(40f, 1.7f) to 0.833f, + Pair(40f, 1.8f) to 0.882f, + Pair(40f, 1.9f) to 0.931f, + Pair(40f, 2.0f) to 0.980f, + Pair(40f, 2.1f) to 1.029f, + Pair(40f, 2.2f) to 1.078f, + Pair(40f, 2.3f) to 1.127f, + Pair(40f, 2.4f) to 1.176f, + Pair(40f, 2.5f) to 1.225f, + Pair(40f, 2.6f) to 1.274f, + Pair(40f, 2.7f) to 1.323f, + Pair(40f, 2.8f) to 1.372f, + Pair(40f, 2.9f) to 1.421f, + Pair(40f, 3.0f) to 1.470f, + Pair(40f, 3.2f) to 1.568f, + Pair(40f, 3.4f) to 1.666f, + Pair(40f, 3.6f) to 1.764f, + Pair(40f, 3.8f) to 1.862f, + Pair(40f, 4.0f) to 1.960f, + Pair(40f, 4.2f) to 2.058f, + Pair(40f, 4.4f) to 2.156f, + Pair(40f, 4.6f) to 2.254f, + Pair(40f, 4.8f) to 2.352f, + Pair(40f, 5.0f) to 2.450f, + Pair(40f, 5.5f) to 2.695f, + Pair(40f, 6.0f) to 2.940f, + Pair(40f, 6.5f) to 3.185f, + Pair(40f, 7.0f) to 3.430f, + Pair(40f, 7.5f) to 3.675f, + Pair(40f, 8.0f) to 3.920f, + Pair(40f, 8.5f) to 4.165f, + Pair(40f, 9.0f) to 4.410f, + Pair(40f, 9.5f) to 4.655f, + Pair(40f, 10.0f) to 4.900f, + Pair(40f, 11.0f) to 5.390f, + Pair(40f, 12.0f) to 5.880f, + Pair(40f, 13.0f) to 6.370f, + Pair(40f, 14.0f) to 6.860f, + Pair(40f, 15.0f) to 7.350f, + Pair(40f, 16.0f) to 7.840f, + Pair(40f, 17.0f) to 8.330f, + Pair(40f, 18.0f) to 8.820f, + Pair(40f, 19.0f) to 9.310f, + Pair(40f, 20.0f) to 9.800f, +) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/const/HzTable_10_10.kt b/app/src/main/java/com/laseroptek/raman/const/HzTable_10_10.kt new file mode 100644 index 0000000..1fec66e --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/HzTable_10_10.kt @@ -0,0 +1,509 @@ +package com.laseroptek.raman.const + +// +// Hz Table3 (HANDPIENCE-10x10) +// - Map, Int> +// - Map, VALUE> +// - Map, HzType(int)> +// + +val HzTable_10_10 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 0.5f) to KEY_BLUE, + Pair(0.5f, 0.6f) to KEY_BLUE, + Pair(0.5f, 0.7f) to KEY_BLUE, + Pair(0.5f, 0.8f) to KEY_BLUE, + Pair(0.5f, 0.9f) to KEY_BLUE, + Pair(0.5f, 1.0f) to KEY_BLUE, + Pair(0.5f, 1.1f) to KEY_YELLOW, + Pair(0.5f, 1.2f) to KEY_YELLOW, + Pair(0.5f, 1.3f) to KEY_YELLOW, + Pair(0.5f, 1.4f) to KEY_YELLOW, + Pair(0.5f, 1.5f) to KEY_YELLOW, + Pair(0.5f, 1.6f) to KEY_YELLOW, + Pair(0.5f, 1.7f) to KEY_YELLOW, + Pair(0.5f, 1.8f) to KEY_YELLOW, + Pair(0.5f, 1.9f) to KEY_YELLOW, + Pair(0.5f, 2.0f) to KEY_YELLOW, + + // Pulse Width = 1f + Pair(1f, 0.5f) to KEY_BLUE, + Pair(1f, 0.6f) to KEY_YELLOW, + Pair(1f, 0.7f) to KEY_YELLOW, + Pair(1f, 0.8f) to KEY_YELLOW, + Pair(1f, 0.9f) to KEY_YELLOW, + Pair(1f, 1.0f) to KEY_YELLOW, + Pair(1f, 1.1f) to KEY_RED, + Pair(1f, 1.2f) to KEY_RED, + Pair(1f, 1.3f) to KEY_RED, + Pair(1f, 1.4f) to KEY_RED, + Pair(1f, 1.5f) to KEY_RED, + Pair(1f, 1.6f) to KEY_RED, + Pair(1f, 1.7f) to KEY_RED, + Pair(1f, 1.8f) to KEY_RED, + Pair(1f, 1.9f) to KEY_RED, + Pair(1f, 2.0f) to KEY_RED, + Pair(1f, 2.1f) to KEY_GRAY, + Pair(1f, 2.2f) to KEY_GRAY, + Pair(1f, 2.3f) to KEY_GRAY, + Pair(1f, 2.4f) to KEY_GRAY, + Pair(1f, 2.5f) to KEY_GRAY, + Pair(1f, 2.6f) to KEY_GRAY, + Pair(1f, 2.7f) to KEY_GRAY, + Pair(1f, 2.8f) to KEY_GRAY, + Pair(1f, 2.9f) to KEY_GRAY, + Pair(1f, 3.0f) to KEY_GRAY, + + // Pulse Width = 1.5f + Pair(1.5f, 0.5f) to KEY_YELLOW, + Pair(1.5f, 0.6f) to KEY_RED, + Pair(1.5f, 0.7f) to KEY_RED, + Pair(1.5f, 0.8f) to KEY_RED, + Pair(1.5f, 0.9f) to KEY_RED, + Pair(1.5f, 1.0f) to KEY_RED, + Pair(1.5f, 1.1f) to KEY_GRAY, + Pair(1.5f, 1.2f) to KEY_GRAY, + Pair(1.5f, 1.3f) to KEY_GRAY, + Pair(1.5f, 1.4f) to KEY_GRAY, + Pair(1.5f, 1.5f) to KEY_GRAY, + Pair(1.5f, 1.6f) to KEY_GRAY, + Pair(1.5f, 1.7f) to KEY_GRAY, + Pair(1.5f, 1.8f) to KEY_GRAY, + Pair(1.5f, 1.9f) to KEY_GRAY, + Pair(1.5f, 2.0f) to KEY_GRAY, + Pair(1.5f, 2.1f) to KEY_GRAY, + Pair(1.5f, 2.2f) to KEY_GRAY, + Pair(1.5f, 2.3f) to KEY_GRAY, + Pair(1.5f, 2.4f) to KEY_GRAY, + Pair(1.5f, 2.5f) to KEY_GRAY, + Pair(1.5f, 2.6f) to KEY_GRAY, + Pair(1.5f, 2.7f) to KEY_GRAY, + Pair(1.5f, 2.8f) to KEY_GRAY, + Pair(1.5f, 2.9f) to KEY_GRAY, + Pair(1.5f, 3.0f) to KEY_GRAY, + Pair(1.5f, 3.2f) to KEY_GRAY, + Pair(1.5f, 3.4f) to KEY_GRAY, + Pair(1.5f, 3.6f) to KEY_GRAY, + Pair(1.5f, 3.8f) to KEY_GRAY, + Pair(1.5f, 4.0f) to KEY_GRAY, + + // Pulse Width = 3f + Pair(3f, 0.5f) to KEY_RED, + Pair(3f, 0.6f) to KEY_GRAY, + Pair(3f, 0.7f) to KEY_GRAY, + Pair(3f, 0.8f) to KEY_GRAY, + Pair(3f, 0.9f) to KEY_GRAY, + Pair(3f, 1.0f) to KEY_GRAY, + Pair(3f, 1.1f) to KEY_GRAY, + Pair(3f, 1.2f) to KEY_GRAY, + Pair(3f, 1.3f) to KEY_GRAY, + Pair(3f, 1.4f) to KEY_GRAY, + Pair(3f, 1.5f) to KEY_GRAY, + Pair(3f, 1.6f) to KEY_GRAY, + Pair(3f, 1.7f) to KEY_GRAY, + Pair(3f, 1.8f) to KEY_GRAY, + Pair(3f, 1.9f) to KEY_GRAY, + Pair(3f, 2.0f) to KEY_GRAY, + Pair(3f, 2.1f) to KEY_GRAY, + Pair(3f, 2.2f) to KEY_GRAY, + Pair(3f, 2.3f) to KEY_GRAY, + Pair(3f, 2.4f) to KEY_GRAY, + Pair(3f, 2.5f) to KEY_GRAY, + Pair(3f, 2.6f) to KEY_GRAY, + Pair(3f, 2.7f) to KEY_GRAY, + Pair(3f, 2.8f) to KEY_GRAY, + Pair(3f, 2.9f) to KEY_GRAY, + Pair(3f, 3.0f) to KEY_GRAY, + Pair(3f, 3.2f) to KEY_GRAY, + Pair(3f, 3.4f) to KEY_GRAY, + Pair(3f, 3.6f) to KEY_GRAY, + Pair(3f, 3.8f) to KEY_GRAY, + Pair(3f, 4.0f) to KEY_GRAY, + Pair(3f, 4.2f) to KEY_GRAY, + Pair(3f, 4.4f) to KEY_GRAY, + Pair(3f, 4.6f) to KEY_GRAY, + Pair(3f, 4.8f) to KEY_GRAY, + Pair(3f, 5.0f) to KEY_GRAY, + Pair(3f, 5.5f) to KEY_GRAY, + Pair(3f, 6.0f) to KEY_GRAY, + + // Pulse Width = 5f + Pair(5f, 0.5f) to KEY_RED, + Pair(5f, 0.6f) to KEY_GRAY, + Pair(5f, 0.7f) to KEY_GRAY, + Pair(5f, 0.8f) to KEY_GRAY, + Pair(5f, 0.9f) to KEY_GRAY, + Pair(5f, 1.0f) to KEY_GRAY, + Pair(5f, 1.1f) to KEY_GRAY, + Pair(5f, 1.2f) to KEY_GRAY, + Pair(5f, 1.3f) to KEY_GRAY, + Pair(5f, 1.4f) to KEY_GRAY, + Pair(5f, 1.5f) to KEY_GRAY, + Pair(5f, 1.6f) to KEY_GRAY, + Pair(5f, 1.7f) to KEY_GRAY, + Pair(5f, 1.8f) to KEY_GRAY, + Pair(5f, 1.9f) to KEY_GRAY, + Pair(5f, 2.0f) to KEY_GRAY, + Pair(5f, 2.1f) to KEY_GRAY, + Pair(5f, 2.2f) to KEY_GRAY, + Pair(5f, 2.3f) to KEY_GRAY, + Pair(5f, 2.4f) to KEY_GRAY, + Pair(5f, 2.5f) to KEY_GRAY, + Pair(5f, 2.6f) to KEY_GRAY, + Pair(5f, 2.7f) to KEY_GRAY, + Pair(5f, 2.8f) to KEY_GRAY, + Pair(5f, 2.9f) to KEY_GRAY, + Pair(5f, 3.0f) to KEY_GRAY, + Pair(5f, 3.2f) to KEY_GRAY, + Pair(5f, 3.4f) to KEY_GRAY, + Pair(5f, 3.6f) to KEY_GRAY, + Pair(5f, 3.8f) to KEY_GRAY, + Pair(5f, 4.0f) to KEY_GRAY, + Pair(5f, 4.2f) to KEY_GRAY, + Pair(5f, 4.4f) to KEY_GRAY, + Pair(5f, 4.6f) to KEY_GRAY, + Pair(5f, 4.8f) to KEY_GRAY, + Pair(5f, 5.0f) to KEY_GRAY, + Pair(5f, 5.5f) to KEY_GRAY, + Pair(5f, 6.0f) to KEY_GRAY, + Pair(5f, 6.5f) to KEY_GRAY, + Pair(5f, 7.0f) to KEY_GRAY, + + // Pulse Width = 10f + Pair(10f, 0.5f) to KEY_GRAY, + Pair(10f, 0.6f) to KEY_GRAY, + Pair(10f, 0.7f) to KEY_GRAY, + Pair(10f, 0.8f) to KEY_GRAY, + Pair(10f, 0.9f) to KEY_GRAY, + Pair(10f, 1.0f) to KEY_GRAY, + Pair(10f, 1.1f) to KEY_GRAY, + Pair(10f, 1.2f) to KEY_GRAY, + Pair(10f, 1.3f) to KEY_GRAY, + Pair(10f, 1.4f) to KEY_GRAY, + Pair(10f, 1.5f) to KEY_GRAY, + Pair(10f, 1.6f) to KEY_GRAY, + Pair(10f, 1.7f) to KEY_GRAY, + Pair(10f, 1.8f) to KEY_GRAY, + Pair(10f, 1.9f) to KEY_GRAY, + Pair(10f, 2.0f) to KEY_GRAY, + Pair(10f, 2.1f) to KEY_GRAY, + Pair(10f, 2.2f) to KEY_GRAY, + Pair(10f, 2.3f) to KEY_GRAY, + Pair(10f, 2.4f) to KEY_GRAY, + Pair(10f, 2.5f) to KEY_GRAY, + Pair(10f, 2.6f) to KEY_GRAY, + Pair(10f, 2.7f) to KEY_GRAY, + Pair(10f, 2.8f) to KEY_GRAY, + Pair(10f, 2.9f) to KEY_GRAY, + Pair(10f, 3.0f) to KEY_GRAY, + Pair(10f, 3.2f) to KEY_GRAY, + Pair(10f, 3.4f) to KEY_GRAY, + Pair(10f, 3.6f) to KEY_GRAY, + Pair(10f, 3.8f) to KEY_GRAY, + Pair(10f, 4.0f) to KEY_GRAY, + Pair(10f, 4.2f) to KEY_GRAY, + Pair(10f, 4.4f) to KEY_GRAY, + Pair(10f, 4.6f) to KEY_GRAY, + Pair(10f, 4.8f) to KEY_GRAY, + Pair(10f, 5.0f) to KEY_GRAY, + Pair(10f, 5.5f) to KEY_GRAY, + Pair(10f, 6.0f) to KEY_GRAY, + Pair(10f, 6.5f) to KEY_GRAY, + Pair(10f, 7.0f) to KEY_GRAY, + Pair(10f, 7.5f) to KEY_GRAY, + Pair(10f, 8.0f) to KEY_GRAY, + Pair(10f, 8.5f) to KEY_GRAY, + Pair(10f, 9.0f) to KEY_GRAY, + Pair(10f, 9.5f) to KEY_GRAY, + Pair(10f, 10.0f) to KEY_GRAY, + + // Pulse Width = 15f + Pair(15f, 0.5f) to KEY_GRAY, + Pair(15f, 0.6f) to KEY_GRAY, + Pair(15f, 0.7f) to KEY_GRAY, + Pair(15f, 0.8f) to KEY_GRAY, + Pair(15f, 0.9f) to KEY_GRAY, + Pair(15f, 1.0f) to KEY_GRAY, + Pair(15f, 1.1f) to KEY_GRAY, + Pair(15f, 1.2f) to KEY_GRAY, + Pair(15f, 1.3f) to KEY_GRAY, + Pair(15f, 1.4f) to KEY_GRAY, + Pair(15f, 1.5f) to KEY_GRAY, + Pair(15f, 1.6f) to KEY_GRAY, + Pair(15f, 1.7f) to KEY_GRAY, + Pair(15f, 1.8f) to KEY_GRAY, + Pair(15f, 1.9f) to KEY_GRAY, + Pair(15f, 2.0f) to KEY_GRAY, + Pair(15f, 2.1f) to KEY_GRAY, + Pair(15f, 2.2f) to KEY_GRAY, + Pair(15f, 2.3f) to KEY_GRAY, + Pair(15f, 2.4f) to KEY_GRAY, + Pair(15f, 2.5f) to KEY_GRAY, + Pair(15f, 2.6f) to KEY_GRAY, + Pair(15f, 2.7f) to KEY_GRAY, + Pair(15f, 2.8f) to KEY_GRAY, + Pair(15f, 2.9f) to KEY_GRAY, + Pair(15f, 3.0f) to KEY_GRAY, + Pair(15f, 3.2f) to KEY_GRAY, + Pair(15f, 3.4f) to KEY_GRAY, + Pair(15f, 3.6f) to KEY_GRAY, + Pair(15f, 3.8f) to KEY_GRAY, + Pair(15f, 4.0f) to KEY_GRAY, + Pair(15f, 4.2f) to KEY_GRAY, + Pair(15f, 4.4f) to KEY_GRAY, + Pair(15f, 4.6f) to KEY_GRAY, + Pair(15f, 4.8f) to KEY_GRAY, + Pair(15f, 5.0f) to KEY_GRAY, + Pair(15f, 5.5f) to KEY_GRAY, + Pair(15f, 6.0f) to KEY_GRAY, + Pair(15f, 6.5f) to KEY_GRAY, + Pair(15f, 7.0f) to KEY_GRAY, + Pair(15f, 7.5f) to KEY_GRAY, + Pair(15f, 8.0f) to KEY_GRAY, + Pair(15f, 8.5f) to KEY_GRAY, + Pair(15f, 9.0f) to KEY_GRAY, + Pair(15f, 9.5f) to KEY_GRAY, + Pair(15f, 10.0f) to KEY_GRAY, + + // Pulse Width = 20f + Pair(20f, 0.5f) to KEY_GRAY, + Pair(20f, 0.6f) to KEY_GRAY, + Pair(20f, 0.7f) to KEY_GRAY, + Pair(20f, 0.8f) to KEY_GRAY, + Pair(20f, 0.9f) to KEY_GRAY, + Pair(20f, 1.0f) to KEY_GRAY, + Pair(20f, 1.1f) to KEY_GRAY, + Pair(20f, 1.2f) to KEY_GRAY, + Pair(20f, 1.3f) to KEY_GRAY, + Pair(20f, 1.4f) to KEY_GRAY, + Pair(20f, 1.5f) to KEY_GRAY, + Pair(20f, 1.6f) to KEY_GRAY, + Pair(20f, 1.7f) to KEY_GRAY, + Pair(20f, 1.8f) to KEY_GRAY, + Pair(20f, 1.9f) to KEY_GRAY, + Pair(20f, 2.0f) to KEY_GRAY, + Pair(20f, 2.1f) to KEY_GRAY, + Pair(20f, 2.2f) to KEY_GRAY, + Pair(20f, 2.3f) to KEY_GRAY, + Pair(20f, 2.4f) to KEY_GRAY, + Pair(20f, 2.5f) to KEY_GRAY, + Pair(20f, 2.6f) to KEY_GRAY, + Pair(20f, 2.7f) to KEY_GRAY, + Pair(20f, 2.8f) to KEY_GRAY, + Pair(20f, 2.9f) to KEY_GRAY, + Pair(20f, 3.0f) to KEY_GRAY, + Pair(20f, 3.2f) to KEY_GRAY, + Pair(20f, 3.4f) to KEY_GRAY, + Pair(20f, 3.6f) to KEY_GRAY, + Pair(20f, 3.8f) to KEY_GRAY, + Pair(20f, 4.0f) to KEY_GRAY, + Pair(20f, 4.2f) to KEY_GRAY, + Pair(20f, 4.4f) to KEY_GRAY, + Pair(20f, 4.6f) to KEY_GRAY, + Pair(20f, 4.8f) to KEY_GRAY, + Pair(20f, 5.0f) to KEY_GRAY, + Pair(20f, 5.5f) to KEY_GRAY, + Pair(20f, 6.0f) to KEY_GRAY, + Pair(20f, 6.5f) to KEY_GRAY, + Pair(20f, 7.0f) to KEY_GRAY, + Pair(20f, 7.5f) to KEY_GRAY, + Pair(20f, 8.0f) to KEY_GRAY, + Pair(20f, 8.5f) to KEY_GRAY, + Pair(20f, 9.0f) to KEY_GRAY, + Pair(20f, 9.5f) to KEY_GRAY, + Pair(20f, 10.0f) to KEY_GRAY, + + // Pulse Width = 25f + Pair(25f, 0.5f) to KEY_GRAY, + Pair(25f, 0.6f) to KEY_GRAY, + Pair(25f, 0.7f) to KEY_GRAY, + Pair(25f, 0.8f) to KEY_GRAY, + Pair(25f, 0.9f) to KEY_GRAY, + Pair(25f, 1.0f) to KEY_GRAY, + Pair(25f, 1.1f) to KEY_GRAY, + Pair(25f, 1.2f) to KEY_GRAY, + Pair(25f, 1.3f) to KEY_GRAY, + Pair(25f, 1.4f) to KEY_GRAY, + Pair(25f, 1.5f) to KEY_GRAY, + Pair(25f, 1.6f) to KEY_GRAY, + Pair(25f, 1.7f) to KEY_GRAY, + Pair(25f, 1.8f) to KEY_GRAY, + Pair(25f, 1.9f) to KEY_GRAY, + Pair(25f, 2.0f) to KEY_GRAY, + Pair(25f, 2.1f) to KEY_GRAY, + Pair(25f, 2.2f) to KEY_GRAY, + Pair(25f, 2.3f) to KEY_GRAY, + Pair(25f, 2.4f) to KEY_GRAY, + Pair(25f, 2.5f) to KEY_GRAY, + Pair(25f, 2.6f) to KEY_GRAY, + Pair(25f, 2.7f) to KEY_GRAY, + Pair(25f, 2.8f) to KEY_GRAY, + Pair(25f, 2.9f) to KEY_GRAY, + Pair(25f, 3.0f) to KEY_GRAY, + Pair(25f, 3.2f) to KEY_GRAY, + Pair(25f, 3.4f) to KEY_GRAY, + Pair(25f, 3.6f) to KEY_GRAY, + Pair(25f, 3.8f) to KEY_GRAY, + Pair(25f, 4.0f) to KEY_GRAY, + Pair(25f, 4.2f) to KEY_GRAY, + Pair(25f, 4.4f) to KEY_GRAY, + Pair(25f, 4.6f) to KEY_GRAY, + Pair(25f, 4.8f) to KEY_GRAY, + Pair(25f, 5.0f) to KEY_GRAY, + Pair(25f, 5.5f) to KEY_GRAY, + Pair(25f, 6.0f) to KEY_GRAY, + Pair(25f, 6.5f) to KEY_GRAY, + Pair(25f, 7.0f) to KEY_GRAY, + Pair(25f, 7.5f) to KEY_GRAY, + Pair(25f, 8.0f) to KEY_GRAY, + Pair(25f, 8.5f) to KEY_GRAY, + Pair(25f, 9.0f) to KEY_GRAY, + Pair(25f, 9.5f) to KEY_GRAY, + Pair(25f, 10.0f) to KEY_GRAY, + + // Pulse Width = 30f + Pair(30f, 0.5f) to KEY_GRAY, + Pair(30f, 0.6f) to KEY_GRAY, + Pair(30f, 0.7f) to KEY_GRAY, + Pair(30f, 0.8f) to KEY_GRAY, + Pair(30f, 0.9f) to KEY_GRAY, + Pair(30f, 1.0f) to KEY_GRAY, + Pair(30f, 1.1f) to KEY_GRAY, + Pair(30f, 1.2f) to KEY_GRAY, + Pair(30f, 1.3f) to KEY_GRAY, + Pair(30f, 1.4f) to KEY_GRAY, + Pair(30f, 1.5f) to KEY_GRAY, + Pair(30f, 1.6f) to KEY_GRAY, + Pair(30f, 1.7f) to KEY_GRAY, + Pair(30f, 1.8f) to KEY_GRAY, + Pair(30f, 1.9f) to KEY_GRAY, + Pair(30f, 2.0f) to KEY_GRAY, + Pair(30f, 2.1f) to KEY_GRAY, + Pair(30f, 2.2f) to KEY_GRAY, + Pair(30f, 2.3f) to KEY_GRAY, + Pair(30f, 2.4f) to KEY_GRAY, + Pair(30f, 2.5f) to KEY_GRAY, + Pair(30f, 2.6f) to KEY_GRAY, + Pair(30f, 2.7f) to KEY_GRAY, + Pair(30f, 2.8f) to KEY_GRAY, + Pair(30f, 2.9f) to KEY_GRAY, + Pair(30f, 3.0f) to KEY_GRAY, + Pair(30f, 3.2f) to KEY_GRAY, + Pair(30f, 3.4f) to KEY_GRAY, + Pair(30f, 3.6f) to KEY_GRAY, + Pair(30f, 3.8f) to KEY_GRAY, + Pair(30f, 4.0f) to KEY_GRAY, + Pair(30f, 4.2f) to KEY_GRAY, + Pair(30f, 4.4f) to KEY_GRAY, + Pair(30f, 4.6f) to KEY_GRAY, + Pair(30f, 4.8f) to KEY_GRAY, + Pair(30f, 5.0f) to KEY_GRAY, + Pair(30f, 5.5f) to KEY_GRAY, + Pair(30f, 6.0f) to KEY_GRAY, + Pair(30f, 6.5f) to KEY_GRAY, + Pair(30f, 7.0f) to KEY_GRAY, + Pair(30f, 7.5f) to KEY_GRAY, + Pair(30f, 8.0f) to KEY_GRAY, + Pair(30f, 8.5f) to KEY_GRAY, + Pair(30f, 9.0f) to KEY_GRAY, + Pair(30f, 9.5f) to KEY_GRAY, + Pair(30f, 10.0f) to KEY_GRAY, + + // Pulse Width = 35f + Pair(35f, 0.5f) to KEY_GRAY, + Pair(35f, 0.6f) to KEY_GRAY, + Pair(35f, 0.7f) to KEY_GRAY, + Pair(35f, 0.8f) to KEY_GRAY, + Pair(35f, 0.9f) to KEY_GRAY, + Pair(35f, 1.0f) to KEY_GRAY, + Pair(35f, 1.1f) to KEY_GRAY, + Pair(35f, 1.2f) to KEY_GRAY, + Pair(35f, 1.3f) to KEY_GRAY, + Pair(35f, 1.4f) to KEY_GRAY, + Pair(35f, 1.5f) to KEY_GRAY, + Pair(35f, 1.6f) to KEY_GRAY, + Pair(35f, 1.7f) to KEY_GRAY, + Pair(35f, 1.8f) to KEY_GRAY, + Pair(35f, 1.9f) to KEY_GRAY, + Pair(35f, 2.0f) to KEY_GRAY, + Pair(35f, 2.1f) to KEY_GRAY, + Pair(35f, 2.2f) to KEY_GRAY, + Pair(35f, 2.3f) to KEY_GRAY, + Pair(35f, 2.4f) to KEY_GRAY, + Pair(35f, 2.5f) to KEY_GRAY, + Pair(35f, 2.6f) to KEY_GRAY, + Pair(35f, 2.7f) to KEY_GRAY, + Pair(35f, 2.8f) to KEY_GRAY, + Pair(35f, 2.9f) to KEY_GRAY, + Pair(35f, 3.0f) to KEY_GRAY, + Pair(35f, 3.2f) to KEY_GRAY, + Pair(35f, 3.4f) to KEY_GRAY, + Pair(35f, 3.6f) to KEY_GRAY, + Pair(35f, 3.8f) to KEY_GRAY, + Pair(35f, 4.0f) to KEY_GRAY, + Pair(35f, 4.2f) to KEY_GRAY, + Pair(35f, 4.4f) to KEY_GRAY, + Pair(35f, 4.6f) to KEY_GRAY, + Pair(35f, 4.8f) to KEY_GRAY, + Pair(35f, 5.0f) to KEY_GRAY, + Pair(35f, 5.5f) to KEY_GRAY, + Pair(35f, 6.0f) to KEY_GRAY, + Pair(35f, 6.5f) to KEY_GRAY, + Pair(35f, 7.0f) to KEY_GRAY, + Pair(35f, 7.5f) to KEY_GRAY, + Pair(35f, 8.0f) to KEY_GRAY, + Pair(35f, 8.5f) to KEY_GRAY, + Pair(35f, 9.0f) to KEY_GRAY, + Pair(35f, 9.5f) to KEY_GRAY, + Pair(35f, 10.0f) to KEY_GRAY, + + // Pulse Width = 40f + Pair(40f, 0.5f) to KEY_GRAY, + Pair(40f, 0.6f) to KEY_GRAY, + Pair(40f, 0.7f) to KEY_GRAY, + Pair(40f, 0.8f) to KEY_GRAY, + Pair(40f, 0.9f) to KEY_GRAY, + Pair(40f, 1.0f) to KEY_GRAY, + Pair(40f, 1.1f) to KEY_GRAY, + Pair(40f, 1.2f) to KEY_GRAY, + Pair(40f, 1.3f) to KEY_GRAY, + Pair(40f, 1.4f) to KEY_GRAY, + Pair(40f, 1.5f) to KEY_GRAY, + Pair(40f, 1.6f) to KEY_GRAY, + Pair(40f, 1.7f) to KEY_GRAY, + Pair(40f, 1.8f) to KEY_GRAY, + Pair(40f, 1.9f) to KEY_GRAY, + Pair(40f, 2.0f) to KEY_GRAY, + Pair(40f, 2.1f) to KEY_GRAY, + Pair(40f, 2.2f) to KEY_GRAY, + Pair(40f, 2.3f) to KEY_GRAY, + Pair(40f, 2.4f) to KEY_GRAY, + Pair(40f, 2.5f) to KEY_GRAY, + Pair(40f, 2.6f) to KEY_GRAY, + Pair(40f, 2.7f) to KEY_GRAY, + Pair(40f, 2.8f) to KEY_GRAY, + Pair(40f, 2.9f) to KEY_GRAY, + Pair(40f, 3.0f) to KEY_GRAY, + Pair(40f, 3.2f) to KEY_GRAY, + Pair(40f, 3.4f) to KEY_GRAY, + Pair(40f, 3.6f) to KEY_GRAY, + Pair(40f, 3.8f) to KEY_GRAY, + Pair(40f, 4.0f) to KEY_GRAY, + Pair(40f, 4.2f) to KEY_GRAY, + Pair(40f, 4.4f) to KEY_GRAY, + Pair(40f, 4.6f) to KEY_GRAY, + Pair(40f, 4.8f) to KEY_GRAY, + Pair(40f, 5.0f) to KEY_GRAY, + Pair(40f, 5.5f) to KEY_GRAY, + Pair(40f, 6.0f) to KEY_GRAY, + Pair(40f, 6.5f) to KEY_GRAY, + Pair(40f, 7.0f) to KEY_GRAY, + Pair(40f, 7.5f) to KEY_GRAY, + Pair(40f, 8.0f) to KEY_GRAY, + Pair(40f, 8.5f) to KEY_GRAY, + Pair(40f, 9.0f) to KEY_GRAY, + Pair(40f, 9.5f) to KEY_GRAY, + Pair(40f, 10.0f) to KEY_GRAY, + + ) + diff --git a/app/src/main/java/com/laseroptek/raman/const/HzTable_12_12.kt b/app/src/main/java/com/laseroptek/raman/const/HzTable_12_12.kt new file mode 100644 index 0000000..8a1b625 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/HzTable_12_12.kt @@ -0,0 +1,434 @@ +package com.laseroptek.raman.const + +// +// Hz Table4 (HANDPIENCE-12x12) +// - Map, Int> +// - Map, VALUE> +// - Map, HzType(Hz)> +// + +val HzTable_12_12 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 0.4f) to KEY_BLUE, + Pair(0.5f, 0.5f) to KEY_BLUE, + Pair(0.5f, 0.6f) to KEY_BLUE, + Pair(0.5f, 0.7f) to KEY_YELLOW, + Pair(0.5f, 0.8f) to KEY_YELLOW, + Pair(0.5f, 0.9f) to KEY_YELLOW, + Pair(0.5f, 1.0f) to KEY_YELLOW, + Pair(0.5f, 1.1f) to KEY_YELLOW, + Pair(0.5f, 1.2f) to KEY_YELLOW, + Pair(0.5f, 1.3f) to KEY_YELLOW, + + // Pulse Width = 1f + Pair(1f, 0.4f) to KEY_YELLOW, + Pair(1f, 0.5f) to KEY_YELLOW, + Pair(1f, 0.6f) to KEY_YELLOW, + Pair(1f, 0.7f) to KEY_RED, + Pair(1f, 0.8f) to KEY_RED, + Pair(1f, 0.9f) to KEY_RED, + Pair(1f, 1.0f) to KEY_RED, + Pair(1f, 1.1f) to KEY_RED, + Pair(1f, 1.2f) to KEY_RED, + Pair(1f, 1.3f) to KEY_RED, + Pair(1f, 1.4f) to KEY_GRAY, + Pair(1f, 1.5f) to KEY_GRAY, + Pair(1f, 1.6f) to KEY_GRAY, + Pair(1f, 1.7f) to KEY_GRAY, + Pair(1f, 1.8f) to KEY_GRAY, + Pair(1f, 1.9f) to KEY_GRAY, + Pair(1f, 2.0f) to KEY_GRAY, + + // Pulse Width = 1.5f + Pair(1.5f, 0.4f) to KEY_RED, + Pair(1.5f, 0.5f) to KEY_RED, + Pair(1.5f, 0.6f) to KEY_RED, + Pair(1.5f, 0.7f) to KEY_GRAY, + Pair(1.5f, 0.8f) to KEY_GRAY, + Pair(1.5f, 0.9f) to KEY_GRAY, + Pair(1.5f, 1.0f) to KEY_GRAY, + Pair(1.5f, 1.1f) to KEY_GRAY, + Pair(1.5f, 1.2f) to KEY_GRAY, + Pair(1.5f, 1.3f) to KEY_GRAY, + Pair(1.5f, 1.4f) to KEY_GRAY, + Pair(1.5f, 1.5f) to KEY_GRAY, + Pair(1.5f, 1.6f) to KEY_GRAY, + Pair(1.5f, 1.7f) to KEY_GRAY, + Pair(1.5f, 1.8f) to KEY_GRAY, + Pair(1.5f, 1.9f) to KEY_GRAY, + Pair(1.5f, 2.0f) to KEY_GRAY, + Pair(1.5f, 2.1f) to KEY_GRAY, + Pair(1.5f, 2.2f) to KEY_GRAY, + Pair(1.5f, 2.3f) to KEY_GRAY, + Pair(1.5f, 2.4f) to KEY_GRAY, + Pair(1.5f, 2.5f) to KEY_GRAY, + Pair(1.5f, 2.6f) to KEY_GRAY, + Pair(1.5f, 2.7f) to KEY_GRAY, + + // Pulse Width = 3f + Pair(3f, 0.4f) to KEY_GRAY, + Pair(3f, 0.5f) to KEY_GRAY, + Pair(3f, 0.6f) to KEY_GRAY, + Pair(3f, 0.7f) to KEY_GRAY, + Pair(3f, 0.8f) to KEY_GRAY, + Pair(3f, 0.9f) to KEY_GRAY, + Pair(3f, 1.0f) to KEY_GRAY, + Pair(3f, 1.1f) to KEY_GRAY, + Pair(3f, 1.2f) to KEY_GRAY, + Pair(3f, 1.3f) to KEY_GRAY, + Pair(3f, 1.4f) to KEY_GRAY, + Pair(3f, 1.5f) to KEY_GRAY, + Pair(3f, 1.6f) to KEY_GRAY, + Pair(3f, 1.7f) to KEY_GRAY, + Pair(3f, 1.8f) to KEY_GRAY, + Pair(3f, 1.9f) to KEY_GRAY, + Pair(3f, 2.0f) to KEY_GRAY, + Pair(3f, 2.1f) to KEY_GRAY, + Pair(3f, 2.2f) to KEY_GRAY, + Pair(3f, 2.3f) to KEY_GRAY, + Pair(3f, 2.4f) to KEY_GRAY, + Pair(3f, 2.5f) to KEY_GRAY, + Pair(3f, 2.6f) to KEY_GRAY, + Pair(3f, 2.7f) to KEY_GRAY, + Pair(3f, 2.8f) to KEY_GRAY, + Pair(3f, 2.9f) to KEY_GRAY, + Pair(3f, 3.0f) to KEY_GRAY, + Pair(3f, 3.2f) to KEY_GRAY, + Pair(3f, 3.4f) to KEY_GRAY, + Pair(3f, 3.6f) to KEY_GRAY, + Pair(3f, 3.8f) to KEY_GRAY, + Pair(3f, 4.0f) to KEY_GRAY, + + // Pulse Width = 5f + Pair(5f, 0.4f) to KEY_GRAY, + Pair(5f, 0.5f) to KEY_GRAY, + Pair(5f, 0.6f) to KEY_GRAY, + Pair(5f, 0.7f) to KEY_GRAY, + Pair(5f, 0.8f) to KEY_GRAY, + Pair(5f, 0.9f) to KEY_GRAY, + Pair(5f, 1.0f) to KEY_GRAY, + Pair(5f, 1.1f) to KEY_GRAY, + Pair(5f, 1.2f) to KEY_GRAY, + Pair(5f, 1.3f) to KEY_GRAY, + Pair(5f, 1.4f) to KEY_GRAY, + Pair(5f, 1.5f) to KEY_GRAY, + Pair(5f, 1.6f) to KEY_GRAY, + Pair(5f, 1.7f) to KEY_GRAY, + Pair(5f, 1.8f) to KEY_GRAY, + Pair(5f, 1.9f) to KEY_GRAY, + Pair(5f, 2.0f) to KEY_GRAY, + Pair(5f, 2.1f) to KEY_GRAY, + Pair(5f, 2.2f) to KEY_GRAY, + Pair(5f, 2.3f) to KEY_GRAY, + Pair(5f, 2.4f) to KEY_GRAY, + Pair(5f, 2.5f) to KEY_GRAY, + Pair(5f, 2.6f) to KEY_GRAY, + Pair(5f, 2.7f) to KEY_GRAY, + Pair(5f, 2.8f) to KEY_GRAY, + Pair(5f, 2.9f) to KEY_GRAY, + Pair(5f, 3.0f) to KEY_GRAY, + Pair(5f, 3.2f) to KEY_GRAY, + Pair(5f, 3.4f) to KEY_GRAY, + Pair(5f, 3.6f) to KEY_GRAY, + Pair(5f, 3.8f) to KEY_GRAY, + Pair(5f, 4.0f) to KEY_GRAY, + Pair(5f, 4.2f) to KEY_GRAY, + Pair(5f, 4.4f) to KEY_GRAY, + Pair(5f, 4.6f) to KEY_GRAY, + Pair(5f, 4.8f) to KEY_GRAY, + + // Pulse Width = 10f + Pair(10f, 0.4f) to KEY_GRAY, + Pair(10f, 0.5f) to KEY_GRAY, + Pair(10f, 0.6f) to KEY_GRAY, + Pair(10f, 0.7f) to KEY_GRAY, + Pair(10f, 0.8f) to KEY_GRAY, + Pair(10f, 0.9f) to KEY_GRAY, + Pair(10f, 1.0f) to KEY_GRAY, + Pair(10f, 1.1f) to KEY_GRAY, + Pair(10f, 1.2f) to KEY_GRAY, + Pair(10f, 1.3f) to KEY_GRAY, + Pair(10f, 1.4f) to KEY_GRAY, + Pair(10f, 1.5f) to KEY_GRAY, + Pair(10f, 1.6f) to KEY_GRAY, + Pair(10f, 1.7f) to KEY_GRAY, + Pair(10f, 1.8f) to KEY_GRAY, + Pair(10f, 1.9f) to KEY_GRAY, + Pair(10f, 2.0f) to KEY_GRAY, + Pair(10f, 2.1f) to KEY_GRAY, + Pair(10f, 2.2f) to KEY_GRAY, + Pair(10f, 2.3f) to KEY_GRAY, + Pair(10f, 2.4f) to KEY_GRAY, + Pair(10f, 2.5f) to KEY_GRAY, + Pair(10f, 2.6f) to KEY_GRAY, + Pair(10f, 2.7f) to KEY_GRAY, + Pair(10f, 2.8f) to KEY_GRAY, + Pair(10f, 2.9f) to KEY_GRAY, + Pair(10f, 3.0f) to KEY_GRAY, + Pair(10f, 3.2f) to KEY_GRAY, + Pair(10f, 3.4f) to KEY_GRAY, + Pair(10f, 3.6f) to KEY_GRAY, + Pair(10f, 3.8f) to KEY_GRAY, + Pair(10f, 4.0f) to KEY_GRAY, + Pair(10f, 4.2f) to KEY_GRAY, + Pair(10f, 4.4f) to KEY_GRAY, + Pair(10f, 4.6f) to KEY_GRAY, + Pair(10f, 4.8f) to KEY_GRAY, + Pair(10f, 5.0f) to KEY_GRAY, + Pair(10f, 5.5f) to KEY_GRAY, + Pair(10f, 6.0f) to KEY_GRAY, + Pair(10f, 6.5f) to KEY_GRAY, + + // Pulse Width = 15f + Pair(15f, 0.4f) to KEY_GRAY, + Pair(15f, 0.5f) to KEY_GRAY, + Pair(15f, 0.6f) to KEY_GRAY, + Pair(15f, 0.7f) to KEY_GRAY, + Pair(15f, 0.8f) to KEY_GRAY, + Pair(15f, 0.9f) to KEY_GRAY, + Pair(15f, 1.0f) to KEY_GRAY, + Pair(15f, 1.1f) to KEY_GRAY, + Pair(15f, 1.2f) to KEY_GRAY, + Pair(15f, 1.3f) to KEY_GRAY, + Pair(15f, 1.4f) to KEY_GRAY, + Pair(15f, 1.5f) to KEY_GRAY, + Pair(15f, 1.6f) to KEY_GRAY, + Pair(15f, 1.7f) to KEY_GRAY, + Pair(15f, 1.8f) to KEY_GRAY, + Pair(15f, 1.9f) to KEY_GRAY, + Pair(15f, 2.0f) to KEY_GRAY, + Pair(15f, 2.1f) to KEY_GRAY, + Pair(15f, 2.2f) to KEY_GRAY, + Pair(15f, 2.3f) to KEY_GRAY, + Pair(15f, 2.4f) to KEY_GRAY, + Pair(15f, 2.5f) to KEY_GRAY, + Pair(15f, 2.6f) to KEY_GRAY, + Pair(15f, 2.7f) to KEY_GRAY, + Pair(15f, 2.8f) to KEY_GRAY, + Pair(15f, 2.9f) to KEY_GRAY, + Pair(15f, 3.0f) to KEY_GRAY, + Pair(15f, 3.2f) to KEY_GRAY, + Pair(15f, 3.4f) to KEY_GRAY, + Pair(15f, 3.6f) to KEY_GRAY, + Pair(15f, 3.8f) to KEY_GRAY, + Pair(15f, 4.0f) to KEY_GRAY, + Pair(15f, 4.2f) to KEY_GRAY, + Pair(15f, 4.4f) to KEY_GRAY, + Pair(15f, 4.6f) to KEY_GRAY, + Pair(15f, 4.8f) to KEY_GRAY, + Pair(15f, 5.0f) to KEY_GRAY, + Pair(15f, 5.5f) to KEY_GRAY, + Pair(15f, 6.0f) to KEY_GRAY, + Pair(15f, 6.5f) to KEY_GRAY, + + // Pulse Width = 20f + Pair(20f, 0.4f) to KEY_GRAY, + Pair(20f, 0.5f) to KEY_GRAY, + Pair(20f, 0.6f) to KEY_GRAY, + Pair(20f, 0.7f) to KEY_GRAY, + Pair(20f, 0.8f) to KEY_GRAY, + Pair(20f, 0.9f) to KEY_GRAY, + Pair(20f, 1.0f) to KEY_GRAY, + Pair(20f, 1.1f) to KEY_GRAY, + Pair(20f, 1.2f) to KEY_GRAY, + Pair(20f, 1.3f) to KEY_GRAY, + Pair(20f, 1.4f) to KEY_GRAY, + Pair(20f, 1.5f) to KEY_GRAY, + Pair(20f, 1.6f) to KEY_GRAY, + Pair(20f, 1.7f) to KEY_GRAY, + Pair(20f, 1.8f) to KEY_GRAY, + Pair(20f, 1.9f) to KEY_GRAY, + Pair(20f, 2.0f) to KEY_GRAY, + Pair(20f, 2.1f) to KEY_GRAY, + Pair(20f, 2.2f) to KEY_GRAY, + Pair(20f, 2.3f) to KEY_GRAY, + Pair(20f, 2.4f) to KEY_GRAY, + Pair(20f, 2.5f) to KEY_GRAY, + Pair(20f, 2.6f) to KEY_GRAY, + Pair(20f, 2.7f) to KEY_GRAY, + Pair(20f, 2.8f) to KEY_GRAY, + Pair(20f, 2.9f) to KEY_GRAY, + Pair(20f, 3.0f) to KEY_GRAY, + Pair(20f, 3.2f) to KEY_GRAY, + Pair(20f, 3.4f) to KEY_GRAY, + Pair(20f, 3.6f) to KEY_GRAY, + Pair(20f, 3.8f) to KEY_GRAY, + Pair(20f, 4.0f) to KEY_GRAY, + Pair(20f, 4.2f) to KEY_GRAY, + Pair(20f, 4.4f) to KEY_GRAY, + Pair(20f, 4.6f) to KEY_GRAY, + Pair(20f, 4.8f) to KEY_GRAY, + Pair(20f, 5.0f) to KEY_GRAY, + Pair(20f, 5.5f) to KEY_GRAY, + Pair(20f, 6.0f) to KEY_GRAY, + Pair(20f, 6.5f) to KEY_GRAY, + + // Pulse Width = 25f + Pair(25f, 0.4f) to KEY_GRAY, + Pair(25f, 0.5f) to KEY_GRAY, + Pair(25f, 0.6f) to KEY_GRAY, + Pair(25f, 0.7f) to KEY_GRAY, + Pair(25f, 0.8f) to KEY_GRAY, + Pair(25f, 0.9f) to KEY_GRAY, + Pair(25f, 1.0f) to KEY_GRAY, + Pair(25f, 1.1f) to KEY_GRAY, + Pair(25f, 1.2f) to KEY_GRAY, + Pair(25f, 1.3f) to KEY_GRAY, + Pair(25f, 1.4f) to KEY_GRAY, + Pair(25f, 1.5f) to KEY_GRAY, + Pair(25f, 1.6f) to KEY_GRAY, + Pair(25f, 1.7f) to KEY_GRAY, + Pair(25f, 1.8f) to KEY_GRAY, + Pair(25f, 1.9f) to KEY_GRAY, + Pair(25f, 2.0f) to KEY_GRAY, + Pair(25f, 2.1f) to KEY_GRAY, + Pair(25f, 2.2f) to KEY_GRAY, + Pair(25f, 2.3f) to KEY_GRAY, + Pair(25f, 2.4f) to KEY_GRAY, + Pair(25f, 2.5f) to KEY_GRAY, + Pair(25f, 2.6f) to KEY_GRAY, + Pair(25f, 2.7f) to KEY_GRAY, + Pair(25f, 2.8f) to KEY_GRAY, + Pair(25f, 2.9f) to KEY_GRAY, + Pair(25f, 3.0f) to KEY_GRAY, + Pair(25f, 3.2f) to KEY_GRAY, + Pair(25f, 3.4f) to KEY_GRAY, + Pair(25f, 3.6f) to KEY_GRAY, + Pair(25f, 3.8f) to KEY_GRAY, + Pair(25f, 4.0f) to KEY_GRAY, + Pair(25f, 4.2f) to KEY_GRAY, + Pair(25f, 4.4f) to KEY_GRAY, + Pair(25f, 4.6f) to KEY_GRAY, + Pair(25f, 4.8f) to KEY_GRAY, + Pair(25f, 5.0f) to KEY_GRAY, + Pair(25f, 5.5f) to KEY_GRAY, + Pair(25f, 6.0f) to KEY_GRAY, + Pair(25f, 6.5f) to KEY_GRAY, + + // Pulse Width = 30f + Pair(30f, 0.4f) to KEY_GRAY, + Pair(30f, 0.5f) to KEY_GRAY, + Pair(30f, 0.6f) to KEY_GRAY, + Pair(30f, 0.7f) to KEY_GRAY, + Pair(30f, 0.8f) to KEY_GRAY, + Pair(30f, 0.9f) to KEY_GRAY, + Pair(30f, 1.0f) to KEY_GRAY, + Pair(30f, 1.1f) to KEY_GRAY, + Pair(30f, 1.2f) to KEY_GRAY, + Pair(30f, 1.3f) to KEY_GRAY, + Pair(30f, 1.4f) to KEY_GRAY, + Pair(30f, 1.5f) to KEY_GRAY, + Pair(30f, 1.6f) to KEY_GRAY, + Pair(30f, 1.7f) to KEY_GRAY, + Pair(30f, 1.8f) to KEY_GRAY, + Pair(30f, 1.9f) to KEY_GRAY, + Pair(30f, 2.0f) to KEY_GRAY, + Pair(30f, 2.1f) to KEY_GRAY, + Pair(30f, 2.2f) to KEY_GRAY, + Pair(30f, 2.3f) to KEY_GRAY, + Pair(30f, 2.4f) to KEY_GRAY, + Pair(30f, 2.5f) to KEY_GRAY, + Pair(30f, 2.6f) to KEY_GRAY, + Pair(30f, 2.7f) to KEY_GRAY, + Pair(30f, 2.8f) to KEY_GRAY, + Pair(30f, 2.9f) to KEY_GRAY, + Pair(30f, 3.0f) to KEY_GRAY, + Pair(30f, 3.2f) to KEY_GRAY, + Pair(30f, 3.4f) to KEY_GRAY, + Pair(30f, 3.6f) to KEY_GRAY, + Pair(30f, 3.8f) to KEY_GRAY, + Pair(30f, 4.0f) to KEY_GRAY, + Pair(30f, 4.2f) to KEY_GRAY, + Pair(30f, 4.4f) to KEY_GRAY, + Pair(30f, 4.6f) to KEY_GRAY, + Pair(30f, 4.8f) to KEY_GRAY, + Pair(30f, 5.0f) to KEY_GRAY, + Pair(30f, 5.5f) to KEY_GRAY, + Pair(30f, 6.0f) to KEY_GRAY, + Pair(30f, 6.5f) to KEY_GRAY, + + // Pulse Width = 35f + Pair(35f, 0.4f) to KEY_GRAY, + Pair(35f, 0.5f) to KEY_GRAY, + Pair(35f, 0.6f) to KEY_GRAY, + Pair(35f, 0.7f) to KEY_GRAY, + Pair(35f, 0.8f) to KEY_GRAY, + Pair(35f, 0.9f) to KEY_GRAY, + Pair(35f, 1.0f) to KEY_GRAY, + Pair(35f, 1.1f) to KEY_GRAY, + Pair(35f, 1.2f) to KEY_GRAY, + Pair(35f, 1.3f) to KEY_GRAY, + Pair(35f, 1.4f) to KEY_GRAY, + Pair(35f, 1.5f) to KEY_GRAY, + Pair(35f, 1.6f) to KEY_GRAY, + Pair(35f, 1.7f) to KEY_GRAY, + Pair(35f, 1.8f) to KEY_GRAY, + Pair(35f, 1.9f) to KEY_GRAY, + Pair(35f, 2.0f) to KEY_GRAY, + Pair(35f, 2.1f) to KEY_GRAY, + Pair(35f, 2.2f) to KEY_GRAY, + Pair(35f, 2.3f) to KEY_GRAY, + Pair(35f, 2.4f) to KEY_GRAY, + Pair(35f, 2.5f) to KEY_GRAY, + Pair(35f, 2.6f) to KEY_GRAY, + Pair(35f, 2.7f) to KEY_GRAY, + Pair(35f, 2.8f) to KEY_GRAY, + Pair(35f, 2.9f) to KEY_GRAY, + Pair(35f, 3.0f) to KEY_GRAY, + Pair(35f, 3.2f) to KEY_GRAY, + Pair(35f, 3.4f) to KEY_GRAY, + Pair(35f, 3.6f) to KEY_GRAY, + Pair(35f, 3.8f) to KEY_GRAY, + Pair(35f, 4.0f) to KEY_GRAY, + Pair(35f, 4.2f) to KEY_GRAY, + Pair(35f, 4.4f) to KEY_GRAY, + Pair(35f, 4.6f) to KEY_GRAY, + Pair(35f, 4.8f) to KEY_GRAY, + Pair(35f, 5.0f) to KEY_GRAY, + Pair(35f, 5.5f) to KEY_GRAY, + Pair(35f, 6.0f) to KEY_GRAY, + Pair(35f, 6.5f) to KEY_GRAY, + + // Pulse Width = 40f + Pair(40f, 0.4f) to KEY_GRAY, + Pair(40f, 0.5f) to KEY_GRAY, + Pair(40f, 0.6f) to KEY_GRAY, + Pair(40f, 0.7f) to KEY_GRAY, + Pair(40f, 0.8f) to KEY_GRAY, + Pair(40f, 0.9f) to KEY_GRAY, + Pair(40f, 1.0f) to KEY_GRAY, + Pair(40f, 1.1f) to KEY_GRAY, + Pair(40f, 1.2f) to KEY_GRAY, + Pair(40f, 1.3f) to KEY_GRAY, + Pair(40f, 1.4f) to KEY_GRAY, + Pair(40f, 1.5f) to KEY_GRAY, + Pair(40f, 1.6f) to KEY_GRAY, + Pair(40f, 1.7f) to KEY_GRAY, + Pair(40f, 1.8f) to KEY_GRAY, + Pair(40f, 1.9f) to KEY_GRAY, + Pair(40f, 2.0f) to KEY_GRAY, + Pair(40f, 2.1f) to KEY_GRAY, + Pair(40f, 2.2f) to KEY_GRAY, + Pair(40f, 2.3f) to KEY_GRAY, + Pair(40f, 2.4f) to KEY_GRAY, + Pair(40f, 2.5f) to KEY_GRAY, + Pair(40f, 2.6f) to KEY_GRAY, + Pair(40f, 2.7f) to KEY_GRAY, + Pair(40f, 2.8f) to KEY_GRAY, + Pair(40f, 2.9f) to KEY_GRAY, + Pair(40f, 3.0f) to KEY_GRAY, + Pair(40f, 3.2f) to KEY_GRAY, + Pair(40f, 3.4f) to KEY_GRAY, + Pair(40f, 3.6f) to KEY_GRAY, + Pair(40f, 3.8f) to KEY_GRAY, + Pair(40f, 4.0f) to KEY_GRAY, + Pair(40f, 4.2f) to KEY_GRAY, + Pair(40f, 4.4f) to KEY_GRAY, + Pair(40f, 4.6f) to KEY_GRAY, + Pair(40f, 4.8f) to KEY_GRAY, + Pair(40f, 5.0f) to KEY_GRAY, + Pair(40f, 5.5f) to KEY_GRAY, + Pair(40f, 6.0f) to KEY_GRAY, + Pair(40f, 6.5f) to KEY_GRAY, +) + diff --git a/app/src/main/java/com/laseroptek/raman/const/HzTable_3_15.kt b/app/src/main/java/com/laseroptek/raman/const/HzTable_3_15.kt new file mode 100644 index 0000000..2d6419f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/HzTable_3_15.kt @@ -0,0 +1,572 @@ +package com.laseroptek.raman.const + +// +// Hz Table5 (HANDPIENCE-3x15) +// - Map, Int> +// - Map, VALUE> +// - Map, HzType(int)> +// + +val HzTable_3_15 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 1.2f) to KEY_BLUE, + Pair(0.5f, 1.3f) to KEY_BLUE, + Pair(0.5f, 1.4f) to KEY_BLUE, + Pair(0.5f, 1.5f) to KEY_BLUE, + Pair(0.5f, 1.6f) to KEY_BLUE, + Pair(0.5f, 1.7f) to KEY_BLUE, + Pair(0.5f, 1.8f) to KEY_BLUE, + Pair(0.5f, 1.9f) to KEY_BLUE, + Pair(0.5f, 2.0f) to KEY_BLUE, + Pair(0.5f, 2.1f) to KEY_BLUE, + Pair(0.5f, 2.2f) to KEY_BLUE, + Pair(0.5f, 2.3f) to KEY_YELLOW, + Pair(0.5f, 2.4f) to KEY_YELLOW, + Pair(0.5f, 2.5f) to KEY_YELLOW, + Pair(0.5f, 2.6f) to KEY_YELLOW, + Pair(0.5f, 2.7f) to KEY_YELLOW, + Pair(0.5f, 2.8f) to KEY_YELLOW, + Pair(0.5f, 2.9f) to KEY_YELLOW, + Pair(0.5f, 3.0f) to KEY_YELLOW, + Pair(0.5f, 3.2f) to KEY_YELLOW, + Pair(0.5f, 3.4f) to KEY_YELLOW, + Pair(0.5f, 3.6f) to KEY_YELLOW, + Pair(0.5f, 3.8f) to KEY_YELLOW, + Pair(0.5f, 4.0f) to KEY_YELLOW, + Pair(0.5f, 4.2f) to KEY_YELLOW, + Pair(0.5f, 4.4f) to KEY_YELLOW, + + // Pulse Width = 1f + Pair(1f, 1.2f) to KEY_YELLOW, + Pair(1f, 1.3f) to KEY_YELLOW, + Pair(1f, 1.4f) to KEY_YELLOW, + Pair(1f, 1.5f) to KEY_YELLOW, + Pair(1f, 1.6f) to KEY_YELLOW, + Pair(1f, 1.7f) to KEY_YELLOW, + Pair(1f, 1.8f) to KEY_YELLOW, + Pair(1f, 1.9f) to KEY_YELLOW, + Pair(1f, 2.0f) to KEY_YELLOW, + Pair(1f, 2.1f) to KEY_YELLOW, + Pair(1f, 2.2f) to KEY_YELLOW, + Pair(1f, 2.3f) to KEY_RED, + Pair(1f, 2.4f) to KEY_RED, + Pair(1f, 2.5f) to KEY_RED, + Pair(1f, 2.6f) to KEY_RED, + Pair(1f, 2.7f) to KEY_RED, + Pair(1f, 2.8f) to KEY_RED, + Pair(1f, 2.9f) to KEY_RED, + Pair(1f, 3.0f) to KEY_RED, + Pair(1f, 3.2f) to KEY_RED, + Pair(1f, 3.4f) to KEY_RED, + Pair(1f, 3.6f) to KEY_RED, + Pair(1f, 3.8f) to KEY_RED, + Pair(1f, 4.0f) to KEY_RED, + Pair(1f, 4.2f) to KEY_RED, + Pair(1f, 4.4f) to KEY_RED, + Pair(1f, 4.6f) to KEY_GRAY, + Pair(1f, 4.8f) to KEY_GRAY, + Pair(1f, 5.0f) to KEY_GRAY, + Pair(1f, 5.5f) to KEY_GRAY, + Pair(1f, 6.0f) to KEY_GRAY, + Pair(1f, 6.5f) to KEY_GRAY, + + // Pulse Width = 1.5f + Pair(1.5f, 1.2f) to KEY_RED, + Pair(1.5f, 1.3f) to KEY_RED, + Pair(1.5f, 1.4f) to KEY_RED, + Pair(1.5f, 1.5f) to KEY_RED, + Pair(1.5f, 1.6f) to KEY_RED, + Pair(1.5f, 1.7f) to KEY_RED, + Pair(1.5f, 1.8f) to KEY_RED, + Pair(1.5f, 1.9f) to KEY_RED, + Pair(1.5f, 2.0f) to KEY_RED, + Pair(1.5f, 2.1f) to KEY_RED, + Pair(1.5f, 2.2f) to KEY_RED, + Pair(1.5f, 2.3f) to KEY_GRAY, + Pair(1.5f, 2.4f) to KEY_GRAY, + Pair(1.5f, 2.5f) to KEY_GRAY, + Pair(1.5f, 2.6f) to KEY_GRAY, + Pair(1.5f, 2.7f) to KEY_GRAY, + Pair(1.5f, 2.8f) to KEY_GRAY, + Pair(1.5f, 2.9f) to KEY_GRAY, + Pair(1.5f, 3.0f) to KEY_GRAY, + Pair(1.5f, 3.2f) to KEY_GRAY, + Pair(1.5f, 3.4f) to KEY_GRAY, + Pair(1.5f, 3.6f) to KEY_GRAY, + Pair(1.5f, 3.8f) to KEY_GRAY, + Pair(1.5f, 4.0f) to KEY_GRAY, + Pair(1.5f, 4.2f) to KEY_GRAY, + Pair(1.5f, 4.4f) to KEY_GRAY, + Pair(1.5f, 4.6f) to KEY_GRAY, + Pair(1.5f, 4.8f) to KEY_GRAY, + Pair(1.5f, 5.0f) to KEY_GRAY, + Pair(1.5f, 5.5f) to KEY_GRAY, + Pair(1.5f, 6.0f) to KEY_GRAY, + Pair(1.5f, 6.5f) to KEY_GRAY, + Pair(1.5f, 7.0f) to KEY_GRAY, + Pair(1.5f, 7.5f) to KEY_GRAY, + Pair(1.5f, 8.0f) to KEY_GRAY, + Pair(1.5f, 8.5f) to KEY_GRAY, + + // Pulse Width = 3f + Pair(3f, 1.2f) to KEY_GRAY, + Pair(3f, 1.3f) to KEY_GRAY, + Pair(3f, 1.4f) to KEY_GRAY, + Pair(3f, 1.5f) to KEY_GRAY, + Pair(3f, 1.6f) to KEY_GRAY, + Pair(3f, 1.7f) to KEY_GRAY, + Pair(3f, 1.8f) to KEY_GRAY, + Pair(3f, 1.9f) to KEY_GRAY, + Pair(3f, 2.0f) to KEY_GRAY, + Pair(3f, 2.1f) to KEY_GRAY, + Pair(3f, 2.2f) to KEY_GRAY, + Pair(3f, 2.3f) to KEY_GRAY, + Pair(3f, 2.4f) to KEY_GRAY, + Pair(3f, 2.5f) to KEY_GRAY, + Pair(3f, 2.6f) to KEY_GRAY, + Pair(3f, 2.7f) to KEY_GRAY, + Pair(3f, 2.8f) to KEY_GRAY, + Pair(3f, 2.9f) to KEY_GRAY, + Pair(3f, 3.0f) to KEY_GRAY, + Pair(3f, 3.2f) to KEY_GRAY, + Pair(3f, 3.4f) to KEY_GRAY, + Pair(3f, 3.6f) to KEY_GRAY, + Pair(3f, 3.8f) to KEY_GRAY, + Pair(3f, 4.0f) to KEY_GRAY, + Pair(3f, 4.2f) to KEY_GRAY, + Pair(3f, 4.4f) to KEY_GRAY, + Pair(3f, 4.6f) to KEY_GRAY, + Pair(3f, 4.8f) to KEY_GRAY, + Pair(3f, 5.0f) to KEY_GRAY, + Pair(3f, 5.5f) to KEY_GRAY, + Pair(3f, 6.0f) to KEY_GRAY, + Pair(3f, 6.5f) to KEY_GRAY, + Pair(3f, 7.0f) to KEY_GRAY, + Pair(3f, 7.5f) to KEY_GRAY, + Pair(3f, 8.0f) to KEY_GRAY, + Pair(3f, 8.5f) to KEY_GRAY, + Pair(3f, 9.0f) to KEY_GRAY, + Pair(3f, 9.5f) to KEY_GRAY, + Pair(3f, 10.0f) to KEY_GRAY, + Pair(3f, 11.0f) to KEY_GRAY, + Pair(3f, 12.0f) to KEY_GRAY, + Pair(3f, 13.0f) to KEY_GRAY, + + // Pulse Width = 5f + Pair(5f, 1.2f) to KEY_GRAY, + Pair(5f, 1.3f) to KEY_GRAY, + Pair(5f, 1.4f) to KEY_GRAY, + Pair(5f, 1.5f) to KEY_GRAY, + Pair(5f, 1.6f) to KEY_GRAY, + Pair(5f, 1.7f) to KEY_GRAY, + Pair(5f, 1.8f) to KEY_GRAY, + Pair(5f, 1.9f) to KEY_GRAY, + Pair(5f, 2.0f) to KEY_GRAY, + Pair(5f, 2.1f) to KEY_GRAY, + Pair(5f, 2.2f) to KEY_GRAY, + Pair(5f, 2.3f) to KEY_GRAY, + Pair(5f, 2.4f) to KEY_GRAY, + Pair(5f, 2.5f) to KEY_GRAY, + Pair(5f, 2.6f) to KEY_GRAY, + Pair(5f, 2.7f) to KEY_GRAY, + Pair(5f, 2.8f) to KEY_GRAY, + Pair(5f, 2.9f) to KEY_GRAY, + Pair(5f, 3.0f) to KEY_GRAY, + Pair(5f, 3.2f) to KEY_GRAY, + Pair(5f, 3.4f) to KEY_GRAY, + Pair(5f, 3.6f) to KEY_GRAY, + Pair(5f, 3.8f) to KEY_GRAY, + Pair(5f, 4.0f) to KEY_GRAY, + Pair(5f, 4.2f) to KEY_GRAY, + Pair(5f, 4.4f) to KEY_GRAY, + Pair(5f, 4.6f) to KEY_GRAY, + Pair(5f, 4.8f) to KEY_GRAY, + Pair(5f, 5.0f) to KEY_GRAY, + Pair(5f, 5.5f) to KEY_GRAY, + Pair(5f, 6.0f) to KEY_GRAY, + Pair(5f, 6.5f) to KEY_GRAY, + Pair(5f, 7.0f) to KEY_GRAY, + Pair(5f, 7.5f) to KEY_GRAY, + Pair(5f, 8.0f) to KEY_GRAY, + Pair(5f, 8.5f) to KEY_GRAY, + Pair(5f, 9.0f) to KEY_GRAY, + Pair(5f, 9.5f) to KEY_GRAY, + Pair(5f, 10.0f) to KEY_GRAY, + Pair(5f, 11.0f) to KEY_GRAY, + Pair(5f, 12.0f) to KEY_GRAY, + Pair(5f, 13.0f) to KEY_GRAY, + Pair(5f, 14.0f) to KEY_GRAY, + Pair(5f, 15.0f) to KEY_GRAY, + + // Pulse Width = 10f + Pair(10f, 1.2f) to KEY_GRAY, + Pair(10f, 1.3f) to KEY_GRAY, + Pair(10f, 1.4f) to KEY_GRAY, + Pair(10f, 1.5f) to KEY_GRAY, + Pair(10f, 1.6f) to KEY_GRAY, + Pair(10f, 1.7f) to KEY_GRAY, + Pair(10f, 1.8f) to KEY_GRAY, + Pair(10f, 1.9f) to KEY_GRAY, + Pair(10f, 2.0f) to KEY_GRAY, + Pair(10f, 2.1f) to KEY_GRAY, + Pair(10f, 2.2f) to KEY_GRAY, + Pair(10f, 2.3f) to KEY_GRAY, + Pair(10f, 2.4f) to KEY_GRAY, + Pair(10f, 2.5f) to KEY_GRAY, + Pair(10f, 2.6f) to KEY_GRAY, + Pair(10f, 2.7f) to KEY_GRAY, + Pair(10f, 2.8f) to KEY_GRAY, + Pair(10f, 2.9f) to KEY_GRAY, + Pair(10f, 3.0f) to KEY_GRAY, + Pair(10f, 3.2f) to KEY_GRAY, + Pair(10f, 3.4f) to KEY_GRAY, + Pair(10f, 3.6f) to KEY_GRAY, + Pair(10f, 3.8f) to KEY_GRAY, + Pair(10f, 4.0f) to KEY_GRAY, + Pair(10f, 4.2f) to KEY_GRAY, + Pair(10f, 4.4f) to KEY_GRAY, + Pair(10f, 4.6f) to KEY_GRAY, + Pair(10f, 4.8f) to KEY_GRAY, + Pair(10f, 5.0f) to KEY_GRAY, + Pair(10f, 5.5f) to KEY_GRAY, + Pair(10f, 6.0f) to KEY_GRAY, + Pair(10f, 6.5f) to KEY_GRAY, + Pair(10f, 7.0f) to KEY_GRAY, + Pair(10f, 7.5f) to KEY_GRAY, + Pair(10f, 8.0f) to KEY_GRAY, + Pair(10f, 8.5f) to KEY_GRAY, + Pair(10f, 9.0f) to KEY_GRAY, + Pair(10f, 9.5f) to KEY_GRAY, + Pair(10f, 10.0f) to KEY_GRAY, + Pair(10f, 11.0f) to KEY_GRAY, + Pair(10f, 12.0f) to KEY_GRAY, + Pair(10f, 13.0f) to KEY_GRAY, + Pair(10f, 14.0f) to KEY_GRAY, + Pair(10f, 15.0f) to KEY_GRAY, + Pair(10f, 16.0f) to KEY_GRAY, + Pair(10f, 17.0f) to KEY_GRAY, + Pair(10f, 18.0f) to KEY_GRAY, + Pair(10f, 19.0f) to KEY_GRAY, + Pair(10f, 20.0f) to KEY_GRAY, + Pair(10f, 21.0f) to KEY_GRAY, + Pair(10f, 22.0f) to KEY_GRAY, + + // Pulse Width = 15f + Pair(15f, 1.2f) to KEY_GRAY, + Pair(15f, 1.3f) to KEY_GRAY, + Pair(15f, 1.4f) to KEY_GRAY, + Pair(15f, 1.5f) to KEY_GRAY, + Pair(15f, 1.6f) to KEY_GRAY, + Pair(15f, 1.7f) to KEY_GRAY, + Pair(15f, 1.8f) to KEY_GRAY, + Pair(15f, 1.9f) to KEY_GRAY, + Pair(15f, 2.0f) to KEY_GRAY, + Pair(15f, 2.1f) to KEY_GRAY, + Pair(15f, 2.2f) to KEY_GRAY, + Pair(15f, 2.3f) to KEY_GRAY, + Pair(15f, 2.4f) to KEY_GRAY, + Pair(15f, 2.5f) to KEY_GRAY, + Pair(15f, 2.6f) to KEY_GRAY, + Pair(15f, 2.7f) to KEY_GRAY, + Pair(15f, 2.8f) to KEY_GRAY, + Pair(15f, 2.9f) to KEY_GRAY, + Pair(15f, 3.0f) to KEY_GRAY, + Pair(15f, 3.2f) to KEY_GRAY, + Pair(15f, 3.4f) to KEY_GRAY, + Pair(15f, 3.6f) to KEY_GRAY, + Pair(15f, 3.8f) to KEY_GRAY, + Pair(15f, 4.0f) to KEY_GRAY, + Pair(15f, 4.2f) to KEY_GRAY, + Pair(15f, 4.4f) to KEY_GRAY, + Pair(15f, 4.6f) to KEY_GRAY, + Pair(15f, 4.8f) to KEY_GRAY, + Pair(15f, 5.0f) to KEY_GRAY, + Pair(15f, 5.5f) to KEY_GRAY, + Pair(15f, 6.0f) to KEY_GRAY, + Pair(15f, 6.5f) to KEY_GRAY, + Pair(15f, 7.0f) to KEY_GRAY, + Pair(15f, 7.5f) to KEY_GRAY, + Pair(15f, 8.0f) to KEY_GRAY, + Pair(15f, 8.5f) to KEY_GRAY, + Pair(15f, 9.0f) to KEY_GRAY, + Pair(15f, 9.5f) to KEY_GRAY, + Pair(15f, 10.0f) to KEY_GRAY, + Pair(15f, 11.0f) to KEY_GRAY, + Pair(15f, 12.0f) to KEY_GRAY, + Pair(15f, 13.0f) to KEY_GRAY, + Pair(15f, 14.0f) to KEY_GRAY, + Pair(15f, 15.0f) to KEY_GRAY, + Pair(15f, 16.0f) to KEY_GRAY, + Pair(15f, 17.0f) to KEY_GRAY, + Pair(15f, 18.0f) to KEY_GRAY, + Pair(15f, 19.0f) to KEY_GRAY, + Pair(15f, 20.0f) to KEY_GRAY, + Pair(15f, 21.0f) to KEY_GRAY, + Pair(15f, 22.0f) to KEY_GRAY, + + // Pulse Width = 20f + Pair(20f, 1.2f) to KEY_GRAY, + Pair(20f, 1.3f) to KEY_GRAY, + Pair(20f, 1.4f) to KEY_GRAY, + Pair(20f, 1.5f) to KEY_GRAY, + Pair(20f, 1.6f) to KEY_GRAY, + Pair(20f, 1.7f) to KEY_GRAY, + Pair(20f, 1.8f) to KEY_GRAY, + Pair(20f, 1.9f) to KEY_GRAY, + Pair(20f, 2.0f) to KEY_GRAY, + Pair(20f, 2.1f) to KEY_GRAY, + Pair(20f, 2.2f) to KEY_GRAY, + Pair(20f, 2.3f) to KEY_GRAY, + Pair(20f, 2.4f) to KEY_GRAY, + Pair(20f, 2.5f) to KEY_GRAY, + Pair(20f, 2.6f) to KEY_GRAY, + Pair(20f, 2.7f) to KEY_GRAY, + Pair(20f, 2.8f) to KEY_GRAY, + Pair(20f, 2.9f) to KEY_GRAY, + Pair(20f, 3.0f) to KEY_GRAY, + Pair(20f, 3.2f) to KEY_GRAY, + Pair(20f, 3.4f) to KEY_GRAY, + Pair(20f, 3.6f) to KEY_GRAY, + Pair(20f, 3.8f) to KEY_GRAY, + Pair(20f, 4.0f) to KEY_GRAY, + Pair(20f, 4.2f) to KEY_GRAY, + Pair(20f, 4.4f) to KEY_GRAY, + Pair(20f, 4.6f) to KEY_GRAY, + Pair(20f, 4.8f) to KEY_GRAY, + Pair(20f, 5.0f) to KEY_GRAY, + Pair(20f, 5.5f) to KEY_GRAY, + Pair(20f, 6.0f) to KEY_GRAY, + Pair(20f, 6.5f) to KEY_GRAY, + Pair(20f, 7.0f) to KEY_GRAY, + Pair(20f, 7.5f) to KEY_GRAY, + Pair(20f, 8.0f) to KEY_GRAY, + Pair(20f, 8.5f) to KEY_GRAY, + Pair(20f, 9.0f) to KEY_GRAY, + Pair(20f, 9.5f) to KEY_GRAY, + Pair(20f, 10.0f) to KEY_GRAY, + Pair(20f, 11.0f) to KEY_GRAY, + Pair(20f, 12.0f) to KEY_GRAY, + Pair(20f, 13.0f) to KEY_GRAY, + Pair(20f, 14.0f) to KEY_GRAY, + Pair(20f, 15.0f) to KEY_GRAY, + Pair(20f, 16.0f) to KEY_GRAY, + Pair(20f, 17.0f) to KEY_GRAY, + Pair(20f, 18.0f) to KEY_GRAY, + Pair(20f, 19.0f) to KEY_GRAY, + Pair(20f, 20.0f) to KEY_GRAY, + Pair(20f, 21.0f) to KEY_GRAY, + Pair(20f, 22.0f) to KEY_GRAY, + + // Pulse Width = 25f + Pair(25f, 1.2f) to KEY_GRAY, + Pair(25f, 1.3f) to KEY_GRAY, + Pair(25f, 1.4f) to KEY_GRAY, + Pair(25f, 1.5f) to KEY_GRAY, + Pair(25f, 1.6f) to KEY_GRAY, + Pair(25f, 1.7f) to KEY_GRAY, + Pair(25f, 1.8f) to KEY_GRAY, + Pair(25f, 1.9f) to KEY_GRAY, + Pair(25f, 2.0f) to KEY_GRAY, + Pair(25f, 2.1f) to KEY_GRAY, + Pair(25f, 2.2f) to KEY_GRAY, + Pair(25f, 2.3f) to KEY_GRAY, + Pair(25f, 2.4f) to KEY_GRAY, + Pair(25f, 2.5f) to KEY_GRAY, + Pair(25f, 2.6f) to KEY_GRAY, + Pair(25f, 2.7f) to KEY_GRAY, + Pair(25f, 2.8f) to KEY_GRAY, + Pair(25f, 2.9f) to KEY_GRAY, + Pair(25f, 3.0f) to KEY_GRAY, + Pair(25f, 3.2f) to KEY_GRAY, + Pair(25f, 3.4f) to KEY_GRAY, + Pair(25f, 3.6f) to KEY_GRAY, + Pair(25f, 3.8f) to KEY_GRAY, + Pair(25f, 4.0f) to KEY_GRAY, + Pair(25f, 4.2f) to KEY_GRAY, + Pair(25f, 4.4f) to KEY_GRAY, + Pair(25f, 4.6f) to KEY_GRAY, + Pair(25f, 4.8f) to KEY_GRAY, + Pair(25f, 5.0f) to KEY_GRAY, + Pair(25f, 5.5f) to KEY_GRAY, + Pair(25f, 6.0f) to KEY_GRAY, + Pair(25f, 6.5f) to KEY_GRAY, + Pair(25f, 7.0f) to KEY_GRAY, + Pair(25f, 7.5f) to KEY_GRAY, + Pair(25f, 8.0f) to KEY_GRAY, + Pair(25f, 8.5f) to KEY_GRAY, + Pair(25f, 9.0f) to KEY_GRAY, + Pair(25f, 9.5f) to KEY_GRAY, + Pair(25f, 10.0f) to KEY_GRAY, + Pair(25f, 11.0f) to KEY_GRAY, + Pair(25f, 12.0f) to KEY_GRAY, + Pair(25f, 13.0f) to KEY_GRAY, + Pair(25f, 14.0f) to KEY_GRAY, + Pair(25f, 15.0f) to KEY_GRAY, + Pair(25f, 16.0f) to KEY_GRAY, + Pair(25f, 17.0f) to KEY_GRAY, + Pair(25f, 18.0f) to KEY_GRAY, + Pair(25f, 19.0f) to KEY_GRAY, + Pair(25f, 20.0f) to KEY_GRAY, + Pair(25f, 21.0f) to KEY_GRAY, + Pair(25f, 22.0f) to KEY_GRAY, + + // Pulse Width = 30f + Pair(30f, 1.2f) to KEY_GRAY, + Pair(30f, 1.3f) to KEY_GRAY, + Pair(30f, 1.4f) to KEY_GRAY, + Pair(30f, 1.5f) to KEY_GRAY, + Pair(30f, 1.6f) to KEY_GRAY, + Pair(30f, 1.7f) to KEY_GRAY, + Pair(30f, 1.8f) to KEY_GRAY, + Pair(30f, 1.9f) to KEY_GRAY, + Pair(30f, 2.0f) to KEY_GRAY, + Pair(30f, 2.1f) to KEY_GRAY, + Pair(30f, 2.2f) to KEY_GRAY, + Pair(30f, 2.3f) to KEY_GRAY, + Pair(30f, 2.4f) to KEY_GRAY, + Pair(30f, 2.5f) to KEY_GRAY, + Pair(30f, 2.6f) to KEY_GRAY, + Pair(30f, 2.7f) to KEY_GRAY, + Pair(30f, 2.8f) to KEY_GRAY, + Pair(30f, 2.9f) to KEY_GRAY, + Pair(30f, 3.0f) to KEY_GRAY, + Pair(30f, 3.2f) to KEY_GRAY, + Pair(30f, 3.4f) to KEY_GRAY, + Pair(30f, 3.6f) to KEY_GRAY, + Pair(30f, 3.8f) to KEY_GRAY, + Pair(30f, 4.0f) to KEY_GRAY, + Pair(30f, 4.2f) to KEY_GRAY, + Pair(30f, 4.4f) to KEY_GRAY, + Pair(30f, 4.6f) to KEY_GRAY, + Pair(30f, 4.8f) to KEY_GRAY, + Pair(30f, 5.0f) to KEY_GRAY, + Pair(30f, 5.5f) to KEY_GRAY, + Pair(30f, 6.0f) to KEY_GRAY, + Pair(30f, 6.5f) to KEY_GRAY, + Pair(30f, 7.0f) to KEY_GRAY, + Pair(30f, 7.5f) to KEY_GRAY, + Pair(30f, 8.0f) to KEY_GRAY, + Pair(30f, 8.5f) to KEY_GRAY, + Pair(30f, 9.0f) to KEY_GRAY, + Pair(30f, 9.5f) to KEY_GRAY, + Pair(30f, 10.0f) to KEY_GRAY, + Pair(30f, 11.0f) to KEY_GRAY, + Pair(30f, 12.0f) to KEY_GRAY, + Pair(30f, 13.0f) to KEY_GRAY, + Pair(30f, 14.0f) to KEY_GRAY, + Pair(30f, 15.0f) to KEY_GRAY, + Pair(30f, 16.0f) to KEY_GRAY, + Pair(30f, 17.0f) to KEY_GRAY, + Pair(30f, 18.0f) to KEY_GRAY, + Pair(30f, 19.0f) to KEY_GRAY, + Pair(30f, 20.0f) to KEY_GRAY, + Pair(30f, 21.0f) to KEY_GRAY, + Pair(30f, 22.0f) to KEY_GRAY, + + // Pulse Width = 35f + Pair(35f, 1.2f) to KEY_GRAY, + Pair(35f, 1.3f) to KEY_GRAY, + Pair(35f, 1.4f) to KEY_GRAY, + Pair(35f, 1.5f) to KEY_GRAY, + Pair(35f, 1.6f) to KEY_GRAY, + Pair(35f, 1.7f) to KEY_GRAY, + Pair(35f, 1.8f) to KEY_GRAY, + Pair(35f, 1.9f) to KEY_GRAY, + Pair(35f, 2.0f) to KEY_GRAY, + Pair(35f, 2.1f) to KEY_GRAY, + Pair(35f, 2.2f) to KEY_GRAY, + Pair(35f, 2.3f) to KEY_GRAY, + Pair(35f, 2.4f) to KEY_GRAY, + Pair(35f, 2.5f) to KEY_GRAY, + Pair(35f, 2.6f) to KEY_GRAY, + Pair(35f, 2.7f) to KEY_GRAY, + Pair(35f, 2.8f) to KEY_GRAY, + Pair(35f, 2.9f) to KEY_GRAY, + Pair(35f, 3.0f) to KEY_GRAY, + Pair(35f, 3.2f) to KEY_GRAY, + Pair(35f, 3.4f) to KEY_GRAY, + Pair(35f, 3.6f) to KEY_GRAY, + Pair(35f, 3.8f) to KEY_GRAY, + Pair(35f, 4.0f) to KEY_GRAY, + Pair(35f, 4.2f) to KEY_GRAY, + Pair(35f, 4.4f) to KEY_GRAY, + Pair(35f, 4.6f) to KEY_GRAY, + Pair(35f, 4.8f) to KEY_GRAY, + Pair(35f, 5.0f) to KEY_GRAY, + Pair(35f, 5.5f) to KEY_GRAY, + Pair(35f, 6.0f) to KEY_GRAY, + Pair(35f, 6.5f) to KEY_GRAY, + Pair(35f, 7.0f) to KEY_GRAY, + Pair(35f, 7.5f) to KEY_GRAY, + Pair(35f, 8.0f) to KEY_GRAY, + Pair(35f, 8.5f) to KEY_GRAY, + Pair(35f, 9.0f) to KEY_GRAY, + Pair(35f, 9.5f) to KEY_GRAY, + Pair(35f, 10.0f) to KEY_GRAY, + Pair(35f, 11.0f) to KEY_GRAY, + Pair(35f, 12.0f) to KEY_GRAY, + Pair(35f, 13.0f) to KEY_GRAY, + Pair(35f, 14.0f) to KEY_GRAY, + Pair(35f, 15.0f) to KEY_GRAY, + Pair(35f, 16.0f) to KEY_GRAY, + Pair(35f, 17.0f) to KEY_GRAY, + Pair(35f, 18.0f) to KEY_GRAY, + Pair(35f, 19.0f) to KEY_GRAY, + Pair(35f, 20.0f) to KEY_GRAY, + Pair(35f, 21.0f) to KEY_GRAY, + Pair(35f, 22.0f) to KEY_GRAY, + + // Pulse Width = 40f + Pair(40f, 1.2f) to KEY_GRAY, + Pair(40f, 1.3f) to KEY_GRAY, + Pair(40f, 1.4f) to KEY_GRAY, + Pair(40f, 1.5f) to KEY_GRAY, + Pair(40f, 1.6f) to KEY_GRAY, + Pair(40f, 1.7f) to KEY_GRAY, + Pair(40f, 1.8f) to KEY_GRAY, + Pair(40f, 1.9f) to KEY_GRAY, + Pair(40f, 2.0f) to KEY_GRAY, + Pair(40f, 2.1f) to KEY_GRAY, + Pair(40f, 2.2f) to KEY_GRAY, + Pair(40f, 2.3f) to KEY_GRAY, + Pair(40f, 2.4f) to KEY_GRAY, + Pair(40f, 2.5f) to KEY_GRAY, + Pair(40f, 2.6f) to KEY_GRAY, + Pair(40f, 2.7f) to KEY_GRAY, + Pair(40f, 2.8f) to KEY_GRAY, + Pair(40f, 2.9f) to KEY_GRAY, + Pair(40f, 3.0f) to KEY_GRAY, + Pair(40f, 3.2f) to KEY_GRAY, + Pair(40f, 3.4f) to KEY_GRAY, + Pair(40f, 3.6f) to KEY_GRAY, + Pair(40f, 3.8f) to KEY_GRAY, + Pair(40f, 4.0f) to KEY_GRAY, + Pair(40f, 4.2f) to KEY_GRAY, + Pair(40f, 4.4f) to KEY_GRAY, + Pair(40f, 4.6f) to KEY_GRAY, + Pair(40f, 4.8f) to KEY_GRAY, + Pair(40f, 5.0f) to KEY_GRAY, + Pair(40f, 5.5f) to KEY_GRAY, + Pair(40f, 6.0f) to KEY_GRAY, + Pair(40f, 6.5f) to KEY_GRAY, + Pair(40f, 7.0f) to KEY_GRAY, + Pair(40f, 7.5f) to KEY_GRAY, + Pair(40f, 8.0f) to KEY_GRAY, + Pair(40f, 8.5f) to KEY_GRAY, + Pair(40f, 9.0f) to KEY_GRAY, + Pair(40f, 9.5f) to KEY_GRAY, + Pair(40f, 10.0f) to KEY_GRAY, + Pair(40f, 11.0f) to KEY_GRAY, + Pair(40f, 12.0f) to KEY_GRAY, + Pair(40f, 13.0f) to KEY_GRAY, + Pair(40f, 14.0f) to KEY_GRAY, + Pair(40f, 15.0f) to KEY_GRAY, + Pair(40f, 16.0f) to KEY_GRAY, + Pair(40f, 17.0f) to KEY_GRAY, + Pair(40f, 18.0f) to KEY_GRAY, + Pair(40f, 19.0f) to KEY_GRAY, + Pair(40f, 20.0f) to KEY_GRAY, + Pair(40f, 21.0f) to KEY_GRAY, + Pair(40f, 22.0f) to KEY_GRAY, +) + diff --git a/app/src/main/java/com/laseroptek/raman/const/HzTable_5_5.kt b/app/src/main/java/com/laseroptek/raman/const/HzTable_5_5.kt new file mode 100644 index 0000000..288d91b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/HzTable_5_5.kt @@ -0,0 +1,653 @@ +package com.laseroptek.raman.const + +// +// Hz Table1 (HANDPIENCE-5x5) +// - Map, Int> +// - Map, VALUE> +// - Map, HzType(int)> +// + +val HzTable_5_5 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f,2.0f) to KEY_GREEN, + Pair(0.5f,2.1f) to KEY_BLUE, + Pair(0.5f,2.2f) to KEY_BLUE, + Pair(0.5f,2.3f) to KEY_BLUE, + Pair(0.5f,2.4f) to KEY_BLUE, + Pair(0.5f,2.5f) to KEY_BLUE, + Pair(0.5f,2.6f) to KEY_BLUE, + Pair(0.5f,2.7f) to KEY_BLUE, + Pair(0.5f,2.8f) to KEY_BLUE, + Pair(0.5f,2.9f) to KEY_BLUE, + Pair(0.5f,3.0f) to KEY_BLUE, + Pair(0.5f,3.2f) to KEY_BLUE, + Pair(0.5f,3.4f) to KEY_BLUE, + Pair(0.5f,3.6f) to KEY_BLUE, + Pair(0.5f,3.8f) to KEY_BLUE, + Pair(0.5f,4.0f) to KEY_YELLOW, + Pair(0.5f,4.2f) to KEY_YELLOW, + Pair(0.5f,4.4f) to KEY_YELLOW, + Pair(0.5f,4.6f) to KEY_YELLOW, + Pair(0.5f,4.8f) to KEY_YELLOW, + Pair(0.5f,5.0f) to KEY_YELLOW, + Pair(0.5f,5.5f) to KEY_YELLOW, + Pair(0.5f,6.0f) to KEY_YELLOW, + Pair(0.5f,6.5f) to KEY_YELLOW, + Pair(0.5f,7.0f) to KEY_YELLOW, + Pair(0.5f,7.5f) to KEY_YELLOW, + Pair(0.5f,8.0f) to KEY_YELLOW, + + // Pulse Width = 1f + Pair(1f, 2.0f) to KEY_BLUE, + Pair(1f, 2.1f) to KEY_YELLOW, + Pair(1f, 2.2f) to KEY_YELLOW, + Pair(1f, 2.3f) to KEY_YELLOW, + Pair(1f, 2.4f) to KEY_YELLOW, + Pair(1f, 2.5f) to KEY_YELLOW, + Pair(1f, 2.6f) to KEY_YELLOW, + Pair(1f, 2.7f) to KEY_YELLOW, + Pair(1f, 2.8f) to KEY_YELLOW, + Pair(1f, 2.9f) to KEY_YELLOW, + Pair(1f, 3.0f) to KEY_YELLOW, + Pair(1f, 3.2f) to KEY_YELLOW, + Pair(1f, 3.4f) to KEY_YELLOW, + Pair(1f, 3.6f) to KEY_YELLOW, + Pair(1f, 3.8f) to KEY_YELLOW, + Pair(1f, 4.0f) to KEY_YELLOW, + Pair(1f, 4.2f) to KEY_RED, + Pair(1f, 4.4f) to KEY_RED, + Pair(1f, 4.6f) to KEY_RED, + Pair(1f, 4.8f) to KEY_RED, + Pair(1f, 5.0f) to KEY_RED, + Pair(1f, 5.5f) to KEY_RED, + Pair(1f, 6.0f) to KEY_RED, + Pair(1f, 6.5f) to KEY_RED, + Pair(1f, 7.0f) to KEY_RED, + Pair(1f, 7.5f) to KEY_RED, + Pair(1f, 8.0f) to KEY_RED, + Pair(1f, 8.5f) to KEY_GRAY, + Pair(1f, 9.0f) to KEY_GRAY, + Pair(1f, 9.5f) to KEY_GRAY, + Pair(1f, 10.0f) to KEY_GRAY, + Pair(1f, 11.0f) to KEY_GRAY, + Pair(1f, 12.0f) to KEY_GRAY, + + // Pulse Width = 1.5f + Pair(1.5f, 2.0f) to KEY_YELLOW, + Pair(1.5f, 2.1f) to KEY_RED, + Pair(1.5f, 2.2f) to KEY_RED, + Pair(1.5f, 2.3f) to KEY_RED, + Pair(1.5f, 2.4f) to KEY_RED, + Pair(1.5f, 2.5f) to KEY_RED, + Pair(1.5f, 2.6f) to KEY_RED, + Pair(1.5f, 2.7f) to KEY_RED, + Pair(1.5f, 2.8f) to KEY_RED, + Pair(1.5f, 2.9f) to KEY_RED, + Pair(1.5f, 3.0f) to KEY_RED, + Pair(1.5f, 3.2f) to KEY_RED, + Pair(1.5f, 3.4f) to KEY_RED, + Pair(1.5f, 3.6f) to KEY_RED, + Pair(1.5f, 3.8f) to KEY_RED, + Pair(1.5f, 4.0f) to KEY_RED, + Pair(1.5f, 4.2f) to KEY_GRAY, + Pair(1.5f, 4.4f) to KEY_GRAY, + Pair(1.5f, 4.6f) to KEY_GRAY, + Pair(1.5f, 4.8f) to KEY_GRAY, + Pair(1.5f, 5.0f) to KEY_GRAY, + Pair(1.5f, 5.5f) to KEY_GRAY, + Pair(1.5f, 6.0f) to KEY_GRAY, + Pair(1.5f, 6.5f) to KEY_GRAY, + Pair(1.5f, 7.0f) to KEY_GRAY, + Pair(1.5f, 7.5f) to KEY_GRAY, + Pair(1.5f, 8.0f) to KEY_GRAY, + Pair(1.5f, 8.5f) to KEY_GRAY, + Pair(1.5f, 9.0f) to KEY_GRAY, + Pair(1.5f, 9.5f) to KEY_GRAY, + Pair(1.5f, 10.0f) to KEY_GRAY, + Pair(1.5f, 11.0f) to KEY_GRAY, + Pair(1.5f, 12.0f) to KEY_GRAY, + Pair(1.5f, 13.0f) to KEY_GRAY, + Pair(1.5f, 14.0f) to KEY_GRAY, + Pair(1.5f, 15.0f) to KEY_GRAY, + Pair(1.5f, 16.0f) to KEY_GRAY, + + // Pulse Width = 3f + Pair(3f, 2.0f ) to KEY_RED, + Pair(3f, 2.1f ) to KEY_GRAY, + Pair(3f, 2.2f ) to KEY_GRAY, + Pair(3f, 2.3f ) to KEY_GRAY, + Pair(3f, 2.4f ) to KEY_GRAY, + Pair(3f, 2.5f ) to KEY_GRAY, + Pair(3f, 2.6f ) to KEY_GRAY, + Pair(3f, 2.7f ) to KEY_GRAY, + Pair(3f, 2.8f ) to KEY_GRAY, + Pair(3f, 2.9f ) to KEY_GRAY, + Pair(3f, 3.0f ) to KEY_GRAY, + Pair(3f, 3.2f ) to KEY_GRAY, + Pair(3f, 3.4f ) to KEY_GRAY, + Pair(3f, 3.6f ) to KEY_GRAY, + Pair(3f, 3.8f ) to KEY_GRAY, + Pair(3f, 4.0f ) to KEY_GRAY, + Pair(3f, 4.2f ) to KEY_GRAY, + Pair(3f, 4.4f ) to KEY_GRAY, + Pair(3f, 4.6f ) to KEY_GRAY, + Pair(3f, 4.8f ) to KEY_GRAY, + Pair(3f, 5.0f ) to KEY_GRAY, + Pair(3f, 5.5f ) to KEY_GRAY, + Pair(3f, 6.0f ) to KEY_GRAY, + Pair(3f, 6.5f ) to KEY_GRAY, + Pair(3f, 7.0f ) to KEY_GRAY, + Pair(3f, 7.5f ) to KEY_GRAY, + Pair(3f, 8.0f ) to KEY_GRAY, + Pair(3f, 8.5f ) to KEY_GRAY, + Pair(3f, 9.0f ) to KEY_GRAY, + Pair(3f, 9.5f ) to KEY_GRAY, + Pair(3f, 10.0f) to KEY_GRAY, + Pair(3f, 11.0f) to KEY_GRAY, + Pair(3f, 12.0f) to KEY_GRAY, + Pair(3f, 13.0f) to KEY_GRAY, + Pair(3f, 14.0f) to KEY_GRAY, + Pair(3f, 15.0f) to KEY_GRAY, + Pair(3f, 16.0f) to KEY_GRAY, + Pair(3f, 17.0f) to KEY_GRAY, + Pair(3f, 18.0f) to KEY_GRAY, + Pair(3f, 19.0f) to KEY_GRAY, + Pair(3f, 20.0f) to KEY_GRAY, + Pair(3f, 21.0f) to KEY_GRAY, + Pair(3f, 22.0f) to KEY_GRAY, + Pair(3f, 23.0f) to KEY_GRAY, + Pair(3f, 24.0f) to KEY_GRAY, + + // Pulse Width = 5f + Pair(5f, 2.0f) to KEY_RED, + Pair(5f, 2.1f) to KEY_GRAY, + Pair(5f, 2.2f) to KEY_GRAY, + Pair(5f, 2.3f) to KEY_GRAY, + Pair(5f, 2.4f) to KEY_GRAY, + Pair(5f, 2.5f) to KEY_GRAY, + Pair(5f, 2.6f) to KEY_GRAY, + Pair(5f, 2.7f) to KEY_GRAY, + Pair(5f, 2.8f) to KEY_GRAY, + Pair(5f, 2.9f) to KEY_GRAY, + Pair(5f, 3.0f) to KEY_GRAY, + Pair(5f, 3.2f) to KEY_GRAY, + Pair(5f, 3.4f) to KEY_GRAY, + Pair(5f, 3.6f) to KEY_GRAY, + Pair(5f, 3.8f) to KEY_GRAY, + Pair(5f, 4.0f) to KEY_GRAY, + Pair(5f, 4.2f) to KEY_GRAY, + Pair(5f, 4.4f) to KEY_GRAY, + Pair(5f, 4.6f) to KEY_GRAY, + Pair(5f, 4.8f) to KEY_GRAY, + Pair(5f, 5.0f) to KEY_GRAY, + Pair(5f, 5.5f) to KEY_GRAY, + Pair(5f, 6.0f) to KEY_GRAY, + Pair(5f, 6.5f) to KEY_GRAY, + Pair(5f, 7.0f) to KEY_GRAY, + Pair(5f, 7.5f) to KEY_GRAY, + Pair(5f, 8.0f) to KEY_GRAY, + Pair(5f, 8.5f) to KEY_GRAY, + Pair(5f, 9.0f) to KEY_GRAY, + Pair(5f, 9.5f) to KEY_GRAY, + Pair(5f, 10.0f) to KEY_GRAY, + Pair(5f, 11.0f) to KEY_GRAY, + Pair(5f, 12.0f) to KEY_GRAY, + Pair(5f, 13.0f) to KEY_GRAY, + Pair(5f, 14.0f) to KEY_GRAY, + Pair(5f, 15.0f) to KEY_GRAY, + Pair(5f, 16.0f) to KEY_GRAY, + Pair(5f, 17.0f) to KEY_GRAY, + Pair(5f, 18.0f) to KEY_GRAY, + Pair(5f, 19.0f) to KEY_GRAY, + Pair(5f, 20.0f) to KEY_GRAY, + Pair(5f, 21.0f) to KEY_GRAY, + Pair(5f, 22.0f) to KEY_GRAY, + Pair(5f, 23.0f) to KEY_GRAY, + Pair(5f, 24.0f) to KEY_GRAY, + Pair(5f, 25.0f) to KEY_GRAY, + Pair(5f, 26.0f) to KEY_GRAY, + Pair(5f, 27.0f) to KEY_GRAY, + Pair(5f, 28.0f) to KEY_GRAY, + + // Pulse Width = 10f + Pair(10f, 2.0f) to KEY_GRAY, + Pair(10f, 2.1f) to KEY_GRAY, + Pair(10f, 2.2f) to KEY_GRAY, + Pair(10f, 2.3f) to KEY_GRAY, + Pair(10f, 2.4f) to KEY_GRAY, + Pair(10f, 2.5f) to KEY_GRAY, + Pair(10f, 2.6f) to KEY_GRAY, + Pair(10f, 2.7f) to KEY_GRAY, + Pair(10f, 2.8f) to KEY_GRAY, + Pair(10f, 2.9f) to KEY_GRAY, + Pair(10f, 3.0f) to KEY_GRAY, + Pair(10f, 3.2f) to KEY_GRAY, + Pair(10f, 3.4f) to KEY_GRAY, + Pair(10f, 3.6f) to KEY_GRAY, + Pair(10f, 3.8f) to KEY_GRAY, + Pair(10f, 4.0f) to KEY_GRAY, + Pair(10f, 4.2f) to KEY_GRAY, + Pair(10f, 4.4f) to KEY_GRAY, + Pair(10f, 4.6f) to KEY_GRAY, + Pair(10f, 4.8f) to KEY_GRAY, + Pair(10f, 5.0f) to KEY_GRAY, + Pair(10f, 5.5f) to KEY_GRAY, + Pair(10f, 6.0f) to KEY_GRAY, + Pair(10f, 6.5f) to KEY_GRAY, + Pair(10f, 7.0f) to KEY_GRAY, + Pair(10f, 7.5f) to KEY_GRAY, + Pair(10f, 8.0f) to KEY_GRAY, + Pair(10f, 8.5f) to KEY_GRAY, + Pair(10f, 9.0f) to KEY_GRAY, + Pair(10f, 9.5f) to KEY_GRAY, + Pair(10f, 10.0f) to KEY_GRAY, + Pair(10f, 11.0f) to KEY_GRAY, + Pair(10f, 12.0f) to KEY_GRAY, + Pair(10f, 13.0f) to KEY_GRAY, + Pair(10f, 14.0f) to KEY_GRAY, + Pair(10f, 15.0f) to KEY_GRAY, + Pair(10f, 16.0f) to KEY_GRAY, + Pair(10f, 17.0f) to KEY_GRAY, + Pair(10f, 18.0f) to KEY_GRAY, + Pair(10f, 19.0f) to KEY_GRAY, + Pair(10f, 20.0f) to KEY_GRAY, + Pair(10f, 21.0f) to KEY_GRAY, + Pair(10f, 22.0f) to KEY_GRAY, + Pair(10f, 23.0f) to KEY_GRAY, + Pair(10f, 24.0f) to KEY_GRAY, + Pair(10f, 25.0f) to KEY_GRAY, + Pair(10f, 26.0f) to KEY_GRAY, + Pair(10f, 27.0f) to KEY_GRAY, + Pair(10f, 28.0f) to KEY_GRAY, + Pair(10f, 29.0f) to KEY_GRAY, + Pair(10f, 30.0f) to KEY_GRAY, + Pair(10f, 31.0f) to KEY_GRAY, + Pair(10f, 32.0f) to KEY_GRAY, + Pair(10f, 33.0f) to KEY_GRAY, + Pair(10f, 34.0f) to KEY_GRAY, + Pair(10f, 35.0f) to KEY_GRAY, + Pair(10f, 36.0f) to KEY_GRAY, + Pair(10f, 37.0f) to KEY_GRAY, + Pair(10f, 38.0f) to KEY_GRAY, + Pair(10f, 39.0f) to KEY_GRAY, + Pair(10f, 40.0f) to KEY_GRAY, + + // Pulse Width = 15f + Pair(15f, 2.0f) to KEY_GRAY, + Pair(15f, 2.1f) to KEY_GRAY, + Pair(15f, 2.2f) to KEY_GRAY, + Pair(15f, 2.3f) to KEY_GRAY, + Pair(15f, 2.4f) to KEY_GRAY, + Pair(15f, 2.5f) to KEY_GRAY, + Pair(15f, 2.6f) to KEY_GRAY, + Pair(15f, 2.7f) to KEY_GRAY, + Pair(15f, 2.8f) to KEY_GRAY, + Pair(15f, 2.9f) to KEY_GRAY, + Pair(15f, 3.0f) to KEY_GRAY, + Pair(15f, 3.2f) to KEY_GRAY, + Pair(15f, 3.4f) to KEY_GRAY, + Pair(15f, 3.6f) to KEY_GRAY, + Pair(15f, 3.8f) to KEY_GRAY, + Pair(15f, 4.0f) to KEY_GRAY, + Pair(15f, 4.2f) to KEY_GRAY, + Pair(15f, 4.4f) to KEY_GRAY, + Pair(15f, 4.6f) to KEY_GRAY, + Pair(15f, 4.8f) to KEY_GRAY, + Pair(15f, 5.0f) to KEY_GRAY, + Pair(15f, 5.5f) to KEY_GRAY, + Pair(15f, 6.0f) to KEY_GRAY, + Pair(15f, 6.5f) to KEY_GRAY, + Pair(15f, 7.0f) to KEY_GRAY, + Pair(15f, 7.5f) to KEY_GRAY, + Pair(15f, 8.0f) to KEY_GRAY, + Pair(15f, 8.5f) to KEY_GRAY, + Pair(15f, 9.0f) to KEY_GRAY, + Pair(15f, 9.5f) to KEY_GRAY, + Pair(15f, 10.0f) to KEY_GRAY, + Pair(15f, 11.0f) to KEY_GRAY, + Pair(15f, 12.0f) to KEY_GRAY, + Pair(15f, 13.0f) to KEY_GRAY, + Pair(15f, 14.0f) to KEY_GRAY, + Pair(15f, 15.0f) to KEY_GRAY, + Pair(15f, 16.0f) to KEY_GRAY, + Pair(15f, 17.0f) to KEY_GRAY, + Pair(15f, 18.0f) to KEY_GRAY, + Pair(15f, 19.0f) to KEY_GRAY, + Pair(15f, 20.0f) to KEY_GRAY, + Pair(15f, 21.0f) to KEY_GRAY, + Pair(15f, 22.0f) to KEY_GRAY, + Pair(15f, 23.0f) to KEY_GRAY, + Pair(15f, 24.0f) to KEY_GRAY, + Pair(15f, 25.0f) to KEY_GRAY, + Pair(15f, 26.0f) to KEY_GRAY, + Pair(15f, 27.0f) to KEY_GRAY, + Pair(15f, 28.0f) to KEY_GRAY, + Pair(15f, 29.0f) to KEY_GRAY, + Pair(15f, 30.0f) to KEY_GRAY, + Pair(15f, 31.0f) to KEY_GRAY, + Pair(15f, 32.0f) to KEY_GRAY, + Pair(15f, 33.0f) to KEY_GRAY, + Pair(15f, 34.0f) to KEY_GRAY, + Pair(15f, 35.0f) to KEY_GRAY, + Pair(15f, 36.0f) to KEY_GRAY, + Pair(15f, 37.0f) to KEY_GRAY, + Pair(15f, 38.0f) to KEY_GRAY, + Pair(15f, 39.0f) to KEY_GRAY, + Pair(15f, 40.0f) to KEY_GRAY, + + // Pulse Width = 20f + Pair(20f, 2.0f) to KEY_GRAY, + Pair(20f, 2.1f) to KEY_GRAY, + Pair(20f, 2.2f) to KEY_GRAY, + Pair(20f, 2.3f) to KEY_GRAY, + Pair(20f, 2.4f) to KEY_GRAY, + Pair(20f, 2.5f) to KEY_GRAY, + Pair(20f, 2.6f) to KEY_GRAY, + Pair(20f, 2.7f) to KEY_GRAY, + Pair(20f, 2.8f) to KEY_GRAY, + Pair(20f, 2.9f) to KEY_GRAY, + Pair(20f, 3.0f) to KEY_GRAY, + Pair(20f, 3.2f) to KEY_GRAY, + Pair(20f, 3.4f) to KEY_GRAY, + Pair(20f, 3.6f) to KEY_GRAY, + Pair(20f, 3.8f) to KEY_GRAY, + Pair(20f, 4.0f) to KEY_GRAY, + Pair(20f, 4.2f) to KEY_GRAY, + Pair(20f, 4.4f) to KEY_GRAY, + Pair(20f, 4.6f) to KEY_GRAY, + Pair(20f, 4.8f) to KEY_GRAY, + Pair(20f, 5.0f) to KEY_GRAY, + Pair(20f, 5.5f) to KEY_GRAY, + Pair(20f, 6.0f) to KEY_GRAY, + Pair(20f, 6.5f) to KEY_GRAY, + Pair(20f, 7.0f) to KEY_GRAY, + Pair(20f, 7.5f) to KEY_GRAY, + Pair(20f, 8.0f) to KEY_GRAY, + Pair(20f, 8.5f) to KEY_GRAY, + Pair(20f, 9.0f) to KEY_GRAY, + Pair(20f, 9.5f) to KEY_GRAY, + Pair(20f, 10.0f) to KEY_GRAY, + Pair(20f, 11.0f) to KEY_GRAY, + Pair(20f, 12.0f) to KEY_GRAY, + Pair(20f, 13.0f) to KEY_GRAY, + Pair(20f, 14.0f) to KEY_GRAY, + Pair(20f, 15.0f) to KEY_GRAY, + Pair(20f, 16.0f) to KEY_GRAY, + Pair(20f, 17.0f) to KEY_GRAY, + Pair(20f, 18.0f) to KEY_GRAY, + Pair(20f, 19.0f) to KEY_GRAY, + Pair(20f, 20.0f) to KEY_GRAY, + Pair(20f, 21.0f) to KEY_GRAY, + Pair(20f, 22.0f) to KEY_GRAY, + Pair(20f, 23.0f) to KEY_GRAY, + Pair(20f, 24.0f) to KEY_GRAY, + Pair(20f, 25.0f) to KEY_GRAY, + Pair(20f, 26.0f) to KEY_GRAY, + Pair(20f, 27.0f) to KEY_GRAY, + Pair(20f, 28.0f) to KEY_GRAY, + Pair(20f, 29.0f) to KEY_GRAY, + Pair(20f, 30.0f) to KEY_GRAY, + Pair(20f, 31.0f) to KEY_GRAY, + Pair(20f, 32.0f) to KEY_GRAY, + Pair(20f, 33.0f) to KEY_GRAY, + Pair(20f, 34.0f) to KEY_GRAY, + Pair(20f, 35.0f) to KEY_GRAY, + Pair(20f, 36.0f) to KEY_GRAY, + Pair(20f, 37.0f) to KEY_GRAY, + Pair(20f, 38.0f) to KEY_GRAY, + Pair(20f, 39.0f) to KEY_GRAY, + Pair(20f, 40.0f) to KEY_GRAY, + + // Pulse Width = 25f + Pair(25f, 2.0f) to KEY_GRAY, + Pair(25f, 2.1f) to KEY_GRAY, + Pair(25f, 2.2f) to KEY_GRAY, + Pair(25f, 2.3f) to KEY_GRAY, + Pair(25f, 2.4f) to KEY_GRAY, + Pair(25f, 2.5f) to KEY_GRAY, + Pair(25f, 2.6f) to KEY_GRAY, + Pair(25f, 2.7f) to KEY_GRAY, + Pair(25f, 2.8f) to KEY_GRAY, + Pair(25f, 2.9f) to KEY_GRAY, + Pair(25f, 3.0f) to KEY_GRAY, + Pair(25f, 3.2f) to KEY_GRAY, + Pair(25f, 3.4f) to KEY_GRAY, + Pair(25f, 3.6f) to KEY_GRAY, + Pair(25f, 3.8f) to KEY_GRAY, + Pair(25f, 4.0f) to KEY_GRAY, + Pair(25f, 4.2f) to KEY_GRAY, + Pair(25f, 4.4f) to KEY_GRAY, + Pair(25f, 4.6f) to KEY_GRAY, + Pair(25f, 4.8f) to KEY_GRAY, + Pair(25f, 5.0f) to KEY_GRAY, + Pair(25f, 5.5f) to KEY_GRAY, + Pair(25f, 6.0f) to KEY_GRAY, + Pair(25f, 6.5f) to KEY_GRAY, + Pair(25f, 7.0f) to KEY_GRAY, + Pair(25f, 7.5f) to KEY_GRAY, + Pair(25f, 8.0f) to KEY_GRAY, + Pair(25f, 8.5f) to KEY_GRAY, + Pair(25f, 9.0f) to KEY_GRAY, + Pair(25f, 9.5f) to KEY_GRAY, + Pair(25f, 10.0f) to KEY_GRAY, + Pair(25f, 11.0f) to KEY_GRAY, + Pair(25f, 12.0f) to KEY_GRAY, + Pair(25f, 13.0f) to KEY_GRAY, + Pair(25f, 14.0f) to KEY_GRAY, + Pair(25f, 15.0f) to KEY_GRAY, + Pair(25f, 16.0f) to KEY_GRAY, + Pair(25f, 17.0f) to KEY_GRAY, + Pair(25f, 18.0f) to KEY_GRAY, + Pair(25f, 19.0f) to KEY_GRAY, + Pair(25f, 20.0f) to KEY_GRAY, + Pair(25f, 21.0f) to KEY_GRAY, + Pair(25f, 22.0f) to KEY_GRAY, + Pair(25f, 23.0f) to KEY_GRAY, + Pair(25f, 24.0f) to KEY_GRAY, + Pair(25f, 25.0f) to KEY_GRAY, + Pair(25f, 26.0f) to KEY_GRAY, + Pair(25f, 27.0f) to KEY_GRAY, + Pair(25f, 28.0f) to KEY_GRAY, + Pair(25f, 29.0f) to KEY_GRAY, + Pair(25f, 30.0f) to KEY_GRAY, + Pair(25f, 31.0f) to KEY_GRAY, + Pair(25f, 32.0f) to KEY_GRAY, + Pair(25f, 33.0f) to KEY_GRAY, + Pair(25f, 34.0f) to KEY_GRAY, + Pair(25f, 35.0f) to KEY_GRAY, + Pair(25f, 36.0f) to KEY_GRAY, + Pair(25f, 37.0f) to KEY_GRAY, + Pair(25f, 38.0f) to KEY_GRAY, + Pair(25f, 39.0f) to KEY_GRAY, + Pair(25f, 40.0f) to KEY_GRAY, + + // Pulse Width = 30f + Pair(30f, 2.0f) to KEY_GRAY, + Pair(30f, 2.1f) to KEY_GRAY, + Pair(30f, 2.2f) to KEY_GRAY, + Pair(30f, 2.3f) to KEY_GRAY, + Pair(30f, 2.4f) to KEY_GRAY, + Pair(30f, 2.5f) to KEY_GRAY, + Pair(30f, 2.6f) to KEY_GRAY, + Pair(30f, 2.7f) to KEY_GRAY, + Pair(30f, 2.8f) to KEY_GRAY, + Pair(30f, 2.9f) to KEY_GRAY, + Pair(30f, 3.0f) to KEY_GRAY, + Pair(30f, 3.2f) to KEY_GRAY, + Pair(30f, 3.4f) to KEY_GRAY, + Pair(30f, 3.6f) to KEY_GRAY, + Pair(30f, 3.8f) to KEY_GRAY, + Pair(30f, 4.0f) to KEY_GRAY, + Pair(30f, 4.2f) to KEY_GRAY, + Pair(30f, 4.4f) to KEY_GRAY, + Pair(30f, 4.6f) to KEY_GRAY, + Pair(30f, 4.8f) to KEY_GRAY, + Pair(30f, 5.0f) to KEY_GRAY, + Pair(30f, 5.5f) to KEY_GRAY, + Pair(30f, 6.0f) to KEY_GRAY, + Pair(30f, 6.5f) to KEY_GRAY, + Pair(30f, 7.0f) to KEY_GRAY, + Pair(30f, 7.5f) to KEY_GRAY, + Pair(30f, 8.0f) to KEY_GRAY, + Pair(30f, 8.5f) to KEY_GRAY, + Pair(30f, 9.0f) to KEY_GRAY, + Pair(30f, 9.5f) to KEY_GRAY, + Pair(30f, 10.0f) to KEY_GRAY, + Pair(30f, 11.0f) to KEY_GRAY, + Pair(30f, 12.0f) to KEY_GRAY, + Pair(30f, 13.0f) to KEY_GRAY, + Pair(30f, 14.0f) to KEY_GRAY, + Pair(30f, 15.0f) to KEY_GRAY, + Pair(30f, 16.0f) to KEY_GRAY, + Pair(30f, 17.0f) to KEY_GRAY, + Pair(30f, 18.0f) to KEY_GRAY, + Pair(30f, 19.0f) to KEY_GRAY, + Pair(30f, 20.0f) to KEY_GRAY, + Pair(30f, 21.0f) to KEY_GRAY, + Pair(30f, 22.0f) to KEY_GRAY, + Pair(30f, 23.0f) to KEY_GRAY, + Pair(30f, 24.0f) to KEY_GRAY, + Pair(30f, 25.0f) to KEY_GRAY, + Pair(30f, 26.0f) to KEY_GRAY, + Pair(30f, 27.0f) to KEY_GRAY, + Pair(30f, 28.0f) to KEY_GRAY, + Pair(30f, 29.0f) to KEY_GRAY, + Pair(30f, 30.0f) to KEY_GRAY, + Pair(30f, 31.0f) to KEY_GRAY, + Pair(30f, 32.0f) to KEY_GRAY, + Pair(30f, 33.0f) to KEY_GRAY, + Pair(30f, 34.0f) to KEY_GRAY, + Pair(30f, 35.0f) to KEY_GRAY, + Pair(30f, 36.0f) to KEY_GRAY, + Pair(30f, 37.0f) to KEY_GRAY, + Pair(30f, 38.0f) to KEY_GRAY, + Pair(30f, 39.0f) to KEY_GRAY, + Pair(30f, 40.0f) to KEY_GRAY, + + // Pulse Width = 35f + Pair(35f, 2.0f) to KEY_GRAY, + Pair(35f, 2.1f) to KEY_GRAY, + Pair(35f, 2.2f) to KEY_GRAY, + Pair(35f, 2.3f) to KEY_GRAY, + Pair(35f, 2.4f) to KEY_GRAY, + Pair(35f, 2.5f) to KEY_GRAY, + Pair(35f, 2.6f) to KEY_GRAY, + Pair(35f, 2.7f) to KEY_GRAY, + Pair(35f, 2.8f) to KEY_GRAY, + Pair(35f, 2.9f) to KEY_GRAY, + Pair(35f, 3.0f) to KEY_GRAY, + Pair(35f, 3.2f) to KEY_GRAY, + Pair(35f, 3.4f) to KEY_GRAY, + Pair(35f, 3.6f) to KEY_GRAY, + Pair(35f, 3.8f) to KEY_GRAY, + Pair(35f, 4.0f) to KEY_GRAY, + Pair(35f, 4.2f) to KEY_GRAY, + Pair(35f, 4.4f) to KEY_GRAY, + Pair(35f, 4.6f) to KEY_GRAY, + Pair(35f, 4.8f) to KEY_GRAY, + Pair(35f, 5.0f) to KEY_GRAY, + Pair(35f, 5.5f) to KEY_GRAY, + Pair(35f, 6.0f) to KEY_GRAY, + Pair(35f, 6.5f) to KEY_GRAY, + Pair(35f, 7.0f) to KEY_GRAY, + Pair(35f, 7.5f) to KEY_GRAY, + Pair(35f, 8.0f) to KEY_GRAY, + Pair(35f, 8.5f) to KEY_GRAY, + Pair(35f, 9.0f) to KEY_GRAY, + Pair(35f, 9.5f) to KEY_GRAY, + Pair(35f, 10.0f) to KEY_GRAY, + Pair(35f, 11.0f) to KEY_GRAY, + Pair(35f, 12.0f) to KEY_GRAY, + Pair(35f, 13.0f) to KEY_GRAY, + Pair(35f, 14.0f) to KEY_GRAY, + Pair(35f, 15.0f) to KEY_GRAY, + Pair(35f, 16.0f) to KEY_GRAY, + Pair(35f, 17.0f) to KEY_GRAY, + Pair(35f, 18.0f) to KEY_GRAY, + Pair(35f, 19.0f) to KEY_GRAY, + Pair(35f, 20.0f) to KEY_GRAY, + Pair(35f, 21.0f) to KEY_GRAY, + Pair(35f, 22.0f) to KEY_GRAY, + Pair(35f, 23.0f) to KEY_GRAY, + Pair(35f, 24.0f) to KEY_GRAY, + Pair(35f, 25.0f) to KEY_GRAY, + Pair(35f, 26.0f) to KEY_GRAY, + Pair(35f, 27.0f) to KEY_GRAY, + Pair(35f, 28.0f) to KEY_GRAY, + Pair(35f, 29.0f) to KEY_GRAY, + Pair(35f, 30.0f) to KEY_GRAY, + Pair(35f, 31.0f) to KEY_GRAY, + Pair(35f, 32.0f) to KEY_GRAY, + Pair(35f, 33.0f) to KEY_GRAY, + Pair(35f, 34.0f) to KEY_GRAY, + Pair(35f, 35.0f) to KEY_GRAY, + Pair(35f, 36.0f) to KEY_GRAY, + Pair(35f, 37.0f) to KEY_GRAY, + Pair(35f, 38.0f) to KEY_GRAY, + Pair(35f, 39.0f) to KEY_GRAY, + Pair(35f, 40.0f) to KEY_GRAY, + + // Pulse Width = 40f + Pair(40f, 2.0f) to KEY_GRAY, + Pair(40f, 2.1f) to KEY_GRAY, + Pair(40f, 2.2f) to KEY_GRAY, + Pair(40f, 2.3f) to KEY_GRAY, + Pair(40f, 2.4f) to KEY_GRAY, + Pair(40f, 2.5f) to KEY_GRAY, + Pair(40f, 2.6f) to KEY_GRAY, + Pair(40f, 2.7f) to KEY_GRAY, + Pair(40f, 2.8f) to KEY_GRAY, + Pair(40f, 2.9f) to KEY_GRAY, + Pair(40f, 3.0f) to KEY_GRAY, + Pair(40f, 3.2f) to KEY_GRAY, + Pair(40f, 3.4f) to KEY_GRAY, + Pair(40f, 3.6f) to KEY_GRAY, + Pair(40f, 3.8f) to KEY_GRAY, + Pair(40f, 4.0f) to KEY_GRAY, + Pair(40f, 4.2f) to KEY_GRAY, + Pair(40f, 4.4f) to KEY_GRAY, + Pair(40f, 4.6f) to KEY_GRAY, + Pair(40f, 4.8f) to KEY_GRAY, + Pair(40f, 5.0f) to KEY_GRAY, + Pair(40f, 5.5f) to KEY_GRAY, + Pair(40f, 6.0f) to KEY_GRAY, + Pair(40f, 6.5f) to KEY_GRAY, + Pair(40f, 7.0f) to KEY_GRAY, + Pair(40f, 7.5f) to KEY_GRAY, + Pair(40f, 8.0f) to KEY_GRAY, + Pair(40f, 8.5f) to KEY_GRAY, + Pair(40f, 9.0f) to KEY_GRAY, + Pair(40f, 9.5f) to KEY_GRAY, + Pair(40f, 10.0f) to KEY_GRAY, + Pair(40f, 11.0f) to KEY_GRAY, + Pair(40f, 12.0f) to KEY_GRAY, + Pair(40f, 13.0f) to KEY_GRAY, + Pair(40f, 14.0f) to KEY_GRAY, + Pair(40f, 15.0f) to KEY_GRAY, + Pair(40f, 16.0f) to KEY_GRAY, + Pair(40f, 17.0f) to KEY_GRAY, + Pair(40f, 18.0f) to KEY_GRAY, + Pair(40f, 19.0f) to KEY_GRAY, + Pair(40f, 20.0f) to KEY_GRAY, + Pair(40f, 21.0f) to KEY_GRAY, + Pair(40f, 22.0f) to KEY_GRAY, + Pair(40f, 23.0f) to KEY_GRAY, + Pair(40f, 24.0f) to KEY_GRAY, + Pair(40f, 25.0f) to KEY_GRAY, + Pair(40f, 26.0f) to KEY_GRAY, + Pair(40f, 27.0f) to KEY_GRAY, + Pair(40f, 28.0f) to KEY_GRAY, + Pair(40f, 29.0f) to KEY_GRAY, + Pair(40f, 30.0f) to KEY_GRAY, + Pair(40f, 31.0f) to KEY_GRAY, + Pair(40f, 32.0f) to KEY_GRAY, + Pair(40f, 33.0f) to KEY_GRAY, + Pair(40f, 34.0f) to KEY_GRAY, + Pair(40f, 35.0f) to KEY_GRAY, + Pair(40f, 36.0f) to KEY_GRAY, + Pair(40f, 37.0f) to KEY_GRAY, + Pair(40f, 38.0f) to KEY_GRAY, + Pair(40f, 39.0f) to KEY_GRAY, + Pair(40f, 40.0f) to KEY_GRAY, +) + diff --git a/app/src/main/java/com/laseroptek/raman/const/HzTable_7_7.kt b/app/src/main/java/com/laseroptek/raman/const/HzTable_7_7.kt new file mode 100644 index 0000000..ea002c7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/HzTable_7_7.kt @@ -0,0 +1,564 @@ +package com.laseroptek.raman.const + +// +// HzTable2 (HANDPIENCE-7x7) +// - Map, Int> +// - Map, VALUE> +// - Map, HzType(int)> +// + +val HzTable_7_7 = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 1.1f) to KEY_BLUE, + Pair(0.5f, 1.2f) to KEY_BLUE, + Pair(0.5f, 1.3f) to KEY_BLUE, + Pair(0.5f, 1.4f) to KEY_BLUE, + Pair(0.5f, 1.5f) to KEY_BLUE, + Pair(0.5f, 1.6f) to KEY_BLUE, + Pair(0.5f, 1.7f) to KEY_BLUE, + Pair(0.5f, 1.8f) to KEY_BLUE, + Pair(0.5f, 1.9f) to KEY_BLUE, + Pair(0.5f, 2.0f) to KEY_BLUE, + Pair(0.5f, 2.1f) to KEY_YELLOW, + Pair(0.5f, 2.2f) to KEY_YELLOW, + Pair(0.5f, 2.3f) to KEY_YELLOW, + Pair(0.5f, 2.4f) to KEY_YELLOW, + Pair(0.5f, 2.5f) to KEY_YELLOW, + Pair(0.5f, 2.6f) to KEY_YELLOW, + Pair(0.5f, 2.7f) to KEY_YELLOW, + Pair(0.5f, 2.8f) to KEY_YELLOW, + Pair(0.5f, 2.9f) to KEY_YELLOW, + Pair(0.5f, 3.0f) to KEY_YELLOW, + Pair(0.5f, 3.2f) to KEY_YELLOW, + Pair(0.5f, 3.4f) to KEY_YELLOW, + Pair(0.5f, 3.6f) to KEY_YELLOW, + Pair(0.5f, 3.8f) to KEY_YELLOW, + Pair(0.5f, 4.0f) to KEY_YELLOW, + + // Pulse Width = 1f + Pair(1f, 1.1f) to KEY_YELLOW, + Pair(1f, 1.2f) to KEY_YELLOW, + Pair(1f, 1.3f) to KEY_YELLOW, + Pair(1f, 1.4f) to KEY_YELLOW, + Pair(1f, 1.5f) to KEY_YELLOW, + Pair(1f, 1.6f) to KEY_YELLOW, + Pair(1f, 1.7f) to KEY_YELLOW, + Pair(1f, 1.8f) to KEY_YELLOW, + Pair(1f, 1.9f) to KEY_YELLOW, + Pair(1f, 2.0f) to KEY_YELLOW, + Pair(1f, 2.1f) to KEY_RED, + Pair(1f, 2.2f) to KEY_RED, + Pair(1f, 2.3f) to KEY_RED, + Pair(1f, 2.4f) to KEY_RED, + Pair(1f, 2.5f) to KEY_RED, + Pair(1f, 2.6f) to KEY_RED, + Pair(1f, 2.7f) to KEY_RED, + Pair(1f, 2.8f) to KEY_RED, + Pair(1f, 2.9f) to KEY_RED, + Pair(1f, 3.0f) to KEY_RED, + Pair(1f, 3.2f) to KEY_RED, + Pair(1f, 3.4f) to KEY_RED, + Pair(1f, 3.6f) to KEY_RED, + Pair(1f, 3.8f) to KEY_RED, + Pair(1f, 4.0f) to KEY_RED, + Pair(1f, 4.2f) to KEY_GRAY, + Pair(1f, 4.4f) to KEY_GRAY, + Pair(1f, 4.6f) to KEY_GRAY, + Pair(1f, 4.8f) to KEY_GRAY, + Pair(1f, 5.0f) to KEY_GRAY, + Pair(1f, 5.5f) to KEY_GRAY, + Pair(1f, 6.0f) to KEY_GRAY, + + // Pulse Width = 1.5f + Pair(1.5f, 1.1f) to KEY_RED, + Pair(1.5f, 1.2f) to KEY_RED, + Pair(1.5f, 1.3f) to KEY_RED, + Pair(1.5f, 1.4f) to KEY_RED, + Pair(1.5f, 1.5f) to KEY_RED, + Pair(1.5f, 1.6f) to KEY_RED, + Pair(1.5f, 1.7f) to KEY_RED, + Pair(1.5f, 1.8f) to KEY_RED, + Pair(1.5f, 1.9f) to KEY_RED, + Pair(1.5f, 2.0f) to KEY_RED, + Pair(1.5f, 2.1f) to KEY_GRAY, + Pair(1.5f, 2.2f) to KEY_GRAY, + Pair(1.5f, 2.3f) to KEY_GRAY, + Pair(1.5f, 2.4f) to KEY_GRAY, + Pair(1.5f, 2.5f) to KEY_GRAY, + Pair(1.5f, 2.6f) to KEY_GRAY, + Pair(1.5f, 2.7f) to KEY_GRAY, + Pair(1.5f, 2.8f) to KEY_GRAY, + Pair(1.5f, 2.9f) to KEY_GRAY, + Pair(1.5f, 3.0f) to KEY_GRAY, + Pair(1.5f, 3.2f) to KEY_GRAY, + Pair(1.5f, 3.4f) to KEY_GRAY, + Pair(1.5f, 3.6f) to KEY_GRAY, + Pair(1.5f, 3.8f) to KEY_GRAY, + Pair(1.5f, 4.0f) to KEY_GRAY, + Pair(1.5f, 4.2f) to KEY_GRAY, + Pair(1.5f, 4.4f) to KEY_GRAY, + Pair(1.5f, 4.6f) to KEY_GRAY, + Pair(1.5f, 4.8f) to KEY_GRAY, + Pair(1.5f, 5.0f) to KEY_GRAY, + Pair(1.5f, 5.5f) to KEY_GRAY, + Pair(1.5f, 6.0f) to KEY_GRAY, + Pair(1.5f, 6.5f) to KEY_GRAY, + Pair(1.5f, 7.0f) to KEY_GRAY, + Pair(1.5f, 7.5f) to KEY_GRAY, + Pair(1.5f, 8.0f) to KEY_GRAY, + + // Pulse Width = 3f + Pair(3f, 1.1f) to KEY_RED, + Pair(3f, 1.2f) to KEY_GRAY, + Pair(3f, 1.3f) to KEY_GRAY, + Pair(3f, 1.4f) to KEY_GRAY, + Pair(3f, 1.5f) to KEY_GRAY, + Pair(3f, 1.6f) to KEY_GRAY, + Pair(3f, 1.7f) to KEY_GRAY, + Pair(3f, 1.8f) to KEY_GRAY, + Pair(3f, 1.9f) to KEY_GRAY, + Pair(3f, 2.0f) to KEY_GRAY, + Pair(3f, 2.1f) to KEY_GRAY, + Pair(3f, 2.2f) to KEY_GRAY, + Pair(3f, 2.3f) to KEY_GRAY, + Pair(3f, 2.4f) to KEY_GRAY, + Pair(3f, 2.5f) to KEY_GRAY, + Pair(3f, 2.6f) to KEY_GRAY, + Pair(3f, 2.7f) to KEY_GRAY, + Pair(3f, 2.8f) to KEY_GRAY, + Pair(3f, 2.9f) to KEY_GRAY, + Pair(3f, 3.0f) to KEY_GRAY, + Pair(3f, 3.2f) to KEY_GRAY, + Pair(3f, 3.4f) to KEY_GRAY, + Pair(3f, 3.6f) to KEY_GRAY, + Pair(3f, 3.8f) to KEY_GRAY, + Pair(3f, 4.0f) to KEY_GRAY, + Pair(3f, 4.2f) to KEY_GRAY, + Pair(3f, 4.4f) to KEY_GRAY, + Pair(3f, 4.6f) to KEY_GRAY, + Pair(3f, 4.8f) to KEY_GRAY, + Pair(3f, 5.0f) to KEY_GRAY, + Pair(3f, 5.5f) to KEY_GRAY, + Pair(3f, 6.0f) to KEY_GRAY, + Pair(3f, 6.5f) to KEY_GRAY, + Pair(3f, 7.0f) to KEY_GRAY, + Pair(3f, 7.5f) to KEY_GRAY, + Pair(3f, 8.0f) to KEY_GRAY, + Pair(3f, 8.5f) to KEY_GRAY, + Pair(3f, 9.0f) to KEY_GRAY, + Pair(3f, 9.5f) to KEY_GRAY, + Pair(3f, 10.0f) to KEY_GRAY, + Pair(3f, 11.0f) to KEY_GRAY, + Pair(3f, 12.0f) to KEY_GRAY, + + // Pulse Width = 5f + Pair(5f, 1.1f) to KEY_GRAY, + Pair(5f, 1.2f) to KEY_GRAY, + Pair(5f, 1.3f) to KEY_GRAY, + Pair(5f, 1.4f) to KEY_GRAY, + Pair(5f, 1.5f) to KEY_GRAY, + Pair(5f, 1.6f) to KEY_GRAY, + Pair(5f, 1.7f) to KEY_GRAY, + Pair(5f, 1.8f) to KEY_GRAY, + Pair(5f, 1.9f) to KEY_GRAY, + Pair(5f, 2.0f) to KEY_GRAY, + Pair(5f, 2.1f) to KEY_GRAY, + Pair(5f, 2.2f) to KEY_GRAY, + Pair(5f, 2.3f) to KEY_GRAY, + Pair(5f, 2.4f) to KEY_GRAY, + Pair(5f, 2.5f) to KEY_GRAY, + Pair(5f, 2.6f) to KEY_GRAY, + Pair(5f, 2.7f) to KEY_GRAY, + Pair(5f, 2.8f) to KEY_GRAY, + Pair(5f, 2.9f) to KEY_GRAY, + Pair(5f, 3.0f) to KEY_GRAY, + Pair(5f, 3.2f) to KEY_GRAY, + Pair(5f, 3.4f) to KEY_GRAY, + Pair(5f, 3.6f) to KEY_GRAY, + Pair(5f, 3.8f) to KEY_GRAY, + Pair(5f, 4.0f) to KEY_GRAY, + Pair(5f, 4.2f) to KEY_GRAY, + Pair(5f, 4.4f) to KEY_GRAY, + Pair(5f, 4.6f) to KEY_GRAY, + Pair(5f, 4.8f) to KEY_GRAY, + Pair(5f, 5.0f) to KEY_GRAY, + Pair(5f, 5.5f) to KEY_GRAY, + Pair(5f, 6.0f) to KEY_GRAY, + Pair(5f, 6.5f) to KEY_GRAY, + Pair(5f, 7.0f) to KEY_GRAY, + Pair(5f, 7.5f) to KEY_GRAY, + Pair(5f, 8.0f) to KEY_GRAY, + Pair(5f, 8.5f) to KEY_GRAY, + Pair(5f, 9.0f) to KEY_GRAY, + Pair(5f, 9.5f) to KEY_GRAY, + Pair(5f, 10.0f) to KEY_GRAY, + Pair(5f, 11.0f) to KEY_GRAY, + Pair(5f, 12.0f) to KEY_GRAY, + Pair(5f, 13.0f) to KEY_GRAY, + Pair(5f, 14.0f) to KEY_GRAY, + + // Pulse Width = 10f + Pair(10f, 1.1f) to KEY_GRAY, + Pair(10f, 1.2f) to KEY_GRAY, + Pair(10f, 1.3f) to KEY_GRAY, + Pair(10f, 1.4f) to KEY_GRAY, + Pair(10f, 1.5f) to KEY_GRAY, + Pair(10f, 1.6f) to KEY_GRAY, + Pair(10f, 1.7f) to KEY_GRAY, + Pair(10f, 1.8f) to KEY_GRAY, + Pair(10f, 1.9f) to KEY_GRAY, + Pair(10f, 2.0f) to KEY_GRAY, + Pair(10f, 2.1f) to KEY_GRAY, + Pair(10f, 2.2f) to KEY_GRAY, + Pair(10f, 2.3f) to KEY_GRAY, + Pair(10f, 2.4f) to KEY_GRAY, + Pair(10f, 2.5f) to KEY_GRAY, + Pair(10f, 2.6f) to KEY_GRAY, + Pair(10f, 2.7f) to KEY_GRAY, + Pair(10f, 2.8f) to KEY_GRAY, + Pair(10f, 2.9f) to KEY_GRAY, + Pair(10f, 3.0f) to KEY_GRAY, + Pair(10f, 3.2f) to KEY_GRAY, + Pair(10f, 3.4f) to KEY_GRAY, + Pair(10f, 3.6f) to KEY_GRAY, + Pair(10f, 3.8f) to KEY_GRAY, + Pair(10f, 4.0f) to KEY_GRAY, + Pair(10f, 4.2f) to KEY_GRAY, + Pair(10f, 4.4f) to KEY_GRAY, + Pair(10f, 4.6f) to KEY_GRAY, + Pair(10f, 4.8f) to KEY_GRAY, + Pair(10f, 5.0f) to KEY_GRAY, + Pair(10f, 5.5f) to KEY_GRAY, + Pair(10f, 6.0f) to KEY_GRAY, + Pair(10f, 6.5f) to KEY_GRAY, + Pair(10f, 7.0f) to KEY_GRAY, + Pair(10f, 7.5f) to KEY_GRAY, + Pair(10f, 8.0f) to KEY_GRAY, + Pair(10f, 8.5f) to KEY_GRAY, + Pair(10f, 9.0f) to KEY_GRAY, + Pair(10f, 9.5f) to KEY_GRAY, + Pair(10f, 10.0f) to KEY_GRAY, + Pair(10f, 11.0f) to KEY_GRAY, + Pair(10f, 12.0f) to KEY_GRAY, + Pair(10f, 13.0f) to KEY_GRAY, + Pair(10f, 14.0f) to KEY_GRAY, + Pair(10f, 15.0f) to KEY_GRAY, + Pair(10f, 16.0f) to KEY_GRAY, + Pair(10f, 17.0f) to KEY_GRAY, + Pair(10f, 18.0f) to KEY_GRAY, + Pair(10f, 19.0f) to KEY_GRAY, + Pair(10f, 20.0f) to KEY_GRAY, + + // Pulse Width = 15f + Pair(15f, 1.1f) to KEY_GRAY, + Pair(15f, 1.2f) to KEY_GRAY, + Pair(15f, 1.3f) to KEY_GRAY, + Pair(15f, 1.4f) to KEY_GRAY, + Pair(15f, 1.5f) to KEY_GRAY, + Pair(15f, 1.6f) to KEY_GRAY, + Pair(15f, 1.7f) to KEY_GRAY, + Pair(15f, 1.8f) to KEY_GRAY, + Pair(15f, 1.9f) to KEY_GRAY, + Pair(15f, 2.0f) to KEY_GRAY, + Pair(15f, 2.1f) to KEY_GRAY, + Pair(15f, 2.2f) to KEY_GRAY, + Pair(15f, 2.3f) to KEY_GRAY, + Pair(15f, 2.4f) to KEY_GRAY, + Pair(15f, 2.5f) to KEY_GRAY, + Pair(15f, 2.6f) to KEY_GRAY, + Pair(15f, 2.7f) to KEY_GRAY, + Pair(15f, 2.8f) to KEY_GRAY, + Pair(15f, 2.9f) to KEY_GRAY, + Pair(15f, 3.0f) to KEY_GRAY, + Pair(15f, 3.2f) to KEY_GRAY, + Pair(15f, 3.4f) to KEY_GRAY, + Pair(15f, 3.6f) to KEY_GRAY, + Pair(15f, 3.8f) to KEY_GRAY, + Pair(15f, 4.0f) to KEY_GRAY, + Pair(15f, 4.2f) to KEY_GRAY, + Pair(15f, 4.4f) to KEY_GRAY, + Pair(15f, 4.6f) to KEY_GRAY, + Pair(15f, 4.8f) to KEY_GRAY, + Pair(15f, 5.0f) to KEY_GRAY, + Pair(15f, 5.5f) to KEY_GRAY, + Pair(15f, 6.0f) to KEY_GRAY, + Pair(15f, 6.5f) to KEY_GRAY, + Pair(15f, 7.0f) to KEY_GRAY, + Pair(15f, 7.5f) to KEY_GRAY, + Pair(15f, 8.0f) to KEY_GRAY, + Pair(15f, 8.5f) to KEY_GRAY, + Pair(15f, 9.0f) to KEY_GRAY, + Pair(15f, 9.5f) to KEY_GRAY, + Pair(15f, 10.0f) to KEY_GRAY, + Pair(15f, 11.0f) to KEY_GRAY, + Pair(15f, 12.0f) to KEY_GRAY, + Pair(15f, 13.0f) to KEY_GRAY, + Pair(15f, 14.0f) to KEY_GRAY, + Pair(15f, 15.0f) to KEY_GRAY, + Pair(15f, 16.0f) to KEY_GRAY, + Pair(15f, 17.0f) to KEY_GRAY, + Pair(15f, 18.0f) to KEY_GRAY, + Pair(15f, 19.0f) to KEY_GRAY, + Pair(15f, 20.0f) to KEY_GRAY, + + // Pulse Width = 20f + Pair(20f, 1.1f) to KEY_GRAY, + Pair(20f, 1.2f) to KEY_GRAY, + Pair(20f, 1.3f) to KEY_GRAY, + Pair(20f, 1.4f) to KEY_GRAY, + Pair(20f, 1.5f) to KEY_GRAY, + Pair(20f, 1.6f) to KEY_GRAY, + Pair(20f, 1.7f) to KEY_GRAY, + Pair(20f, 1.8f) to KEY_GRAY, + Pair(20f, 1.9f) to KEY_GRAY, + Pair(20f, 2.0f) to KEY_GRAY, + Pair(20f, 2.1f) to KEY_GRAY, + Pair(20f, 2.2f) to KEY_GRAY, + Pair(20f, 2.3f) to KEY_GRAY, + Pair(20f, 2.4f) to KEY_GRAY, + Pair(20f, 2.5f) to KEY_GRAY, + Pair(20f, 2.6f) to KEY_GRAY, + Pair(20f, 2.7f) to KEY_GRAY, + Pair(20f, 2.8f) to KEY_GRAY, + Pair(20f, 2.9f) to KEY_GRAY, + Pair(20f, 3.0f) to KEY_GRAY, + Pair(20f, 3.2f) to KEY_GRAY, + Pair(20f, 3.4f) to KEY_GRAY, + Pair(20f, 3.6f) to KEY_GRAY, + Pair(20f, 3.8f) to KEY_GRAY, + Pair(20f, 4.0f) to KEY_GRAY, + Pair(20f, 4.2f) to KEY_GRAY, + Pair(20f, 4.4f) to KEY_GRAY, + Pair(20f, 4.6f) to KEY_GRAY, + Pair(20f, 4.8f) to KEY_GRAY, + Pair(20f, 5.0f) to KEY_GRAY, + Pair(20f, 5.5f) to KEY_GRAY, + Pair(20f, 6.0f) to KEY_GRAY, + Pair(20f, 6.5f) to KEY_GRAY, + Pair(20f, 7.0f) to KEY_GRAY, + Pair(20f, 7.5f) to KEY_GRAY, + Pair(20f, 8.0f) to KEY_GRAY, + Pair(20f, 8.5f) to KEY_GRAY, + Pair(20f, 9.0f) to KEY_GRAY, + Pair(20f, 9.5f) to KEY_GRAY, + Pair(20f, 10.0f) to KEY_GRAY, + Pair(20f, 11.0f) to KEY_GRAY, + Pair(20f, 12.0f) to KEY_GRAY, + Pair(20f, 13.0f) to KEY_GRAY, + Pair(20f, 14.0f) to KEY_GRAY, + Pair(20f, 15.0f) to KEY_GRAY, + Pair(20f, 16.0f) to KEY_GRAY, + Pair(20f, 17.0f) to KEY_GRAY, + Pair(20f, 18.0f) to KEY_GRAY, + Pair(20f, 19.0f) to KEY_GRAY, + Pair(20f, 20.0f) to KEY_GRAY, + + // Pulse Width = 25f + Pair(25f, 1.1f) to KEY_GRAY, + Pair(25f, 1.2f) to KEY_GRAY, + Pair(25f, 1.3f) to KEY_GRAY, + Pair(25f, 1.4f) to KEY_GRAY, + Pair(25f, 1.5f) to KEY_GRAY, + Pair(25f, 1.6f) to KEY_GRAY, + Pair(25f, 1.7f) to KEY_GRAY, + Pair(25f, 1.8f) to KEY_GRAY, + Pair(25f, 1.9f) to KEY_GRAY, + Pair(25f, 2.0f) to KEY_GRAY, + Pair(25f, 2.1f) to KEY_GRAY, + Pair(25f, 2.2f) to KEY_GRAY, + Pair(25f, 2.3f) to KEY_GRAY, + Pair(25f, 2.4f) to KEY_GRAY, + Pair(25f, 2.5f) to KEY_GRAY, + Pair(25f, 2.6f) to KEY_GRAY, + Pair(25f, 2.7f) to KEY_GRAY, + Pair(25f, 2.8f) to KEY_GRAY, + Pair(25f, 2.9f) to KEY_GRAY, + Pair(25f, 3.0f) to KEY_GRAY, + Pair(25f, 3.2f) to KEY_GRAY, + Pair(25f, 3.4f) to KEY_GRAY, + Pair(25f, 3.6f) to KEY_GRAY, + Pair(25f, 3.8f) to KEY_GRAY, + Pair(25f, 4.0f) to KEY_GRAY, + Pair(25f, 4.2f) to KEY_GRAY, + Pair(25f, 4.4f) to KEY_GRAY, + Pair(25f, 4.6f) to KEY_GRAY, + Pair(25f, 4.8f) to KEY_GRAY, + Pair(25f, 5.0f) to KEY_GRAY, + Pair(25f, 5.5f) to KEY_GRAY, + Pair(25f, 6.0f) to KEY_GRAY, + Pair(25f, 6.5f) to KEY_GRAY, + Pair(25f, 7.0f) to KEY_GRAY, + Pair(25f, 7.5f) to KEY_GRAY, + Pair(25f, 8.0f) to KEY_GRAY, + Pair(25f, 8.5f) to KEY_GRAY, + Pair(25f, 9.0f) to KEY_GRAY, + Pair(25f, 9.5f) to KEY_GRAY, + Pair(25f, 10.0f) to KEY_GRAY, + Pair(25f, 11.0f) to KEY_GRAY, + Pair(25f, 12.0f) to KEY_GRAY, + Pair(25f, 13.0f) to KEY_GRAY, + Pair(25f, 14.0f) to KEY_GRAY, + Pair(25f, 15.0f) to KEY_GRAY, + Pair(25f, 16.0f) to KEY_GRAY, + Pair(25f, 17.0f) to KEY_GRAY, + Pair(25f, 18.0f) to KEY_GRAY, + Pair(25f, 19.0f) to KEY_GRAY, + Pair(25f, 20.0f) to KEY_GRAY, + + // Pulse Width = 30f + Pair(30f, 1.1f) to KEY_GRAY, + Pair(30f, 1.2f) to KEY_GRAY, + Pair(30f, 1.3f) to KEY_GRAY, + Pair(30f, 1.4f) to KEY_GRAY, + Pair(30f, 1.5f) to KEY_GRAY, + Pair(30f, 1.6f) to KEY_GRAY, + Pair(30f, 1.7f) to KEY_GRAY, + Pair(30f, 1.8f) to KEY_GRAY, + Pair(30f, 1.9f) to KEY_GRAY, + Pair(30f, 2.0f) to KEY_GRAY, + Pair(30f, 2.1f) to KEY_GRAY, + Pair(30f, 2.2f) to KEY_GRAY, + Pair(30f, 2.3f) to KEY_GRAY, + Pair(30f, 2.4f) to KEY_GRAY, + Pair(30f, 2.5f) to KEY_GRAY, + Pair(30f, 2.6f) to KEY_GRAY, + Pair(30f, 2.7f) to KEY_GRAY, + Pair(30f, 2.8f) to KEY_GRAY, + Pair(30f, 2.9f) to KEY_GRAY, + Pair(30f, 3.0f) to KEY_GRAY, + Pair(30f, 3.2f) to KEY_GRAY, + Pair(30f, 3.4f) to KEY_GRAY, + Pair(30f, 3.6f) to KEY_GRAY, + Pair(30f, 3.8f) to KEY_GRAY, + Pair(30f, 4.0f) to KEY_GRAY, + Pair(30f, 4.2f) to KEY_GRAY, + Pair(30f, 4.4f) to KEY_GRAY, + Pair(30f, 4.6f) to KEY_GRAY, + Pair(30f, 4.8f) to KEY_GRAY, + Pair(30f, 5.0f) to KEY_GRAY, + Pair(30f, 5.5f) to KEY_GRAY, + Pair(30f, 6.0f) to KEY_GRAY, + Pair(30f, 6.5f) to KEY_GRAY, + Pair(30f, 7.0f) to KEY_GRAY, + Pair(30f, 7.5f) to KEY_GRAY, + Pair(30f, 8.0f) to KEY_GRAY, + Pair(30f, 8.5f) to KEY_GRAY, + Pair(30f, 9.0f) to KEY_GRAY, + Pair(30f, 9.5f) to KEY_GRAY, + Pair(30f, 10.0f) to KEY_GRAY, + Pair(30f, 11.0f) to KEY_GRAY, + Pair(30f, 12.0f) to KEY_GRAY, + Pair(30f, 13.0f) to KEY_GRAY, + Pair(30f, 14.0f) to KEY_GRAY, + Pair(30f, 15.0f) to KEY_GRAY, + Pair(30f, 16.0f) to KEY_GRAY, + Pair(30f, 17.0f) to KEY_GRAY, + Pair(30f, 18.0f) to KEY_GRAY, + Pair(30f, 19.0f) to KEY_GRAY, + Pair(30f, 20.0f) to KEY_GRAY, + + // Pulse Width = 35f + Pair(35f, 1.1f) to KEY_GRAY, + Pair(35f, 1.2f) to KEY_GRAY, + Pair(35f, 1.3f) to KEY_GRAY, + Pair(35f, 1.4f) to KEY_GRAY, + Pair(35f, 1.5f) to KEY_GRAY, + Pair(35f, 1.6f) to KEY_GRAY, + Pair(35f, 1.7f) to KEY_GRAY, + Pair(35f, 1.8f) to KEY_GRAY, + Pair(35f, 1.9f) to KEY_GRAY, + Pair(35f, 2.0f) to KEY_GRAY, + Pair(35f, 2.1f) to KEY_GRAY, + Pair(35f, 2.2f) to KEY_GRAY, + Pair(35f, 2.3f) to KEY_GRAY, + Pair(35f, 2.4f) to KEY_GRAY, + Pair(35f, 2.5f) to KEY_GRAY, + Pair(35f, 2.6f) to KEY_GRAY, + Pair(35f, 2.7f) to KEY_GRAY, + Pair(35f, 2.8f) to KEY_GRAY, + Pair(35f, 2.9f) to KEY_GRAY, + Pair(35f, 3.0f) to KEY_GRAY, + Pair(35f, 3.2f) to KEY_GRAY, + Pair(35f, 3.4f) to KEY_GRAY, + Pair(35f, 3.6f) to KEY_GRAY, + Pair(35f, 3.8f) to KEY_GRAY, + Pair(35f, 4.0f) to KEY_GRAY, + Pair(35f, 4.2f) to KEY_GRAY, + Pair(35f, 4.4f) to KEY_GRAY, + Pair(35f, 4.6f) to KEY_GRAY, + Pair(35f, 4.8f) to KEY_GRAY, + Pair(35f, 5.0f) to KEY_GRAY, + Pair(35f, 5.5f) to KEY_GRAY, + Pair(35f, 6.0f) to KEY_GRAY, + Pair(35f, 6.5f) to KEY_GRAY, + Pair(35f, 7.0f) to KEY_GRAY, + Pair(35f, 7.5f) to KEY_GRAY, + Pair(35f, 8.0f) to KEY_GRAY, + Pair(35f, 8.5f) to KEY_GRAY, + Pair(35f, 9.0f) to KEY_GRAY, + Pair(35f, 9.5f) to KEY_GRAY, + Pair(35f, 10.0f) to KEY_GRAY, + Pair(35f, 11.0f) to KEY_GRAY, + Pair(35f, 12.0f) to KEY_GRAY, + Pair(35f, 13.0f) to KEY_GRAY, + Pair(35f, 14.0f) to KEY_GRAY, + Pair(35f, 15.0f) to KEY_GRAY, + Pair(35f, 16.0f) to KEY_GRAY, + Pair(35f, 17.0f) to KEY_GRAY, + Pair(35f, 18.0f) to KEY_GRAY, + Pair(35f, 19.0f) to KEY_GRAY, + Pair(35f, 20.0f) to KEY_GRAY, + + // Pulse Width = 40f + Pair(40f, 1.1f) to KEY_GRAY, + Pair(40f, 1.2f) to KEY_GRAY, + Pair(40f, 1.3f) to KEY_GRAY, + Pair(40f, 1.4f) to KEY_GRAY, + Pair(40f, 1.5f) to KEY_GRAY, + Pair(40f, 1.6f) to KEY_GRAY, + Pair(40f, 1.7f) to KEY_GRAY, + Pair(40f, 1.8f) to KEY_GRAY, + Pair(40f, 1.9f) to KEY_GRAY, + Pair(40f, 2.0f) to KEY_GRAY, + Pair(40f, 2.1f) to KEY_GRAY, + Pair(40f, 2.2f) to KEY_GRAY, + Pair(40f, 2.3f) to KEY_GRAY, + Pair(40f, 2.4f) to KEY_GRAY, + Pair(40f, 2.5f) to KEY_GRAY, + Pair(40f, 2.6f) to KEY_GRAY, + Pair(40f, 2.7f) to KEY_GRAY, + Pair(40f, 2.8f) to KEY_GRAY, + Pair(40f, 2.9f) to KEY_GRAY, + Pair(40f, 3.0f) to KEY_GRAY, + Pair(40f, 3.2f) to KEY_GRAY, + Pair(40f, 3.4f) to KEY_GRAY, + Pair(40f, 3.6f) to KEY_GRAY, + Pair(40f, 3.8f) to KEY_GRAY, + Pair(40f, 4.0f) to KEY_GRAY, + Pair(40f, 4.2f) to KEY_GRAY, + Pair(40f, 4.4f) to KEY_GRAY, + Pair(40f, 4.6f) to KEY_GRAY, + Pair(40f, 4.8f) to KEY_GRAY, + Pair(40f, 5.0f) to KEY_GRAY, + Pair(40f, 5.5f) to KEY_GRAY, + Pair(40f, 6.0f) to KEY_GRAY, + Pair(40f, 6.5f) to KEY_GRAY, + Pair(40f, 7.0f) to KEY_GRAY, + Pair(40f, 7.5f) to KEY_GRAY, + Pair(40f, 8.0f) to KEY_GRAY, + Pair(40f, 8.5f) to KEY_GRAY, + Pair(40f, 9.0f) to KEY_GRAY, + Pair(40f, 9.5f) to KEY_GRAY, + Pair(40f, 10.0f) to KEY_GRAY, + Pair(40f, 11.0f) to KEY_GRAY, + Pair(40f, 12.0f) to KEY_GRAY, + Pair(40f, 13.0f) to KEY_GRAY, + Pair(40f, 14.0f) to KEY_GRAY, + Pair(40f, 15.0f) to KEY_GRAY, + Pair(40f, 16.0f) to KEY_GRAY, + Pair(40f, 17.0f) to KEY_GRAY, + Pair(40f, 18.0f) to KEY_GRAY, + Pair(40f, 19.0f) to KEY_GRAY, + Pair(40f, 20.0f) to KEY_GRAY, +) + diff --git a/app/src/main/java/com/laseroptek/raman/const/Pallas3Const.kt b/app/src/main/java/com/laseroptek/raman/const/Pallas3Const.kt new file mode 100644 index 0000000..fc16e44 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/Pallas3Const.kt @@ -0,0 +1,490 @@ +package com.laseroptek.raman.const + +import androidx.compose.ui.graphics.Color +import com.laseroptek.raman.R +import com.laseroptek.raman.data.model.IconButtonItem +import com.laseroptek.raman.data.model.Preset +import com.laseroptek.raman.data.model.serial.SprayDcd +import com.laseroptek.raman.utils.ext.formatWithThousandComma + +/////////////////////////////////////////////////////////// +// GLOBAL CONST + +const val SHOW_ENERGY_DETECT_CLOSE = true +const val SHOW_ENERGY_DETECT = true + +const val DATABASE_VERSION = 1 +const val ENGINEER_PIN_NUNBER = "064589" +const val txRepeatMillis = 1 * 1000L // tx repeat (millis) : 1 sec (for real) vs 60 sec (for dev) +const val preHeatingMillis = 5 * 60 * 1000L // 5 * 60 * 1000L +const val SERIAL_PORT_NAME = "/dev/ttyS0" +const val SERIAL_BAUDRATE = 38400 + + +/////////////////////////////////////////////////////////// +// MIN MAX DEFAULT CONST + +const val MIN_GUIDE_BEAM = 0 +const val DEFAULT_GUIDE_BEAM = 1000 +const val MAX_GUIDE_BEAM = 2000 + +const val MAX_LAMP_COUNT = 999999999 +const val MAX_LASER_COUNT = 999999999 +const val MAX_OP_TIME = 999999999 + +const val MAX_CHART_COLUMN_DATA_SIZE = 25 + +const val DEFAULT_LIFETIME_LAMP = 1000000 +const val DEFAULT_LIFETIME_HP = 1000000 +const val DEFAULT_LIFETIME_DETECTOR = 999999 +const val DEFAULT_LIFETIME_WATER = 999999 + +const val MAX_LIFETIME_LAMP = 1000000 +const val MAX_LIFETIME_HP = 1000000 +const val MAX_LIFETIME_DETECTOR = 999999 +const val MAX_LIFETIME_WATER = 999999 + +const val DEFAULT_REFER2_GOOD = 0.10f +const val DEFAULT_REFER2_ACCEPTABLE = 0.25f +const val DEFAULT_REFER2_BAD = 0.25f + +const val MAX_REFER2_VALUE = 100 // 100% (= 1.0f) + +const val MAX_TEMP_INT_DIGIT = 3 // 3 자리수 (xxx) +const val MAX_TEMP_DECIMAL_DIGIT = 1 // 1 자리수 (.x) +const val MAX_TEMP_VALUE = 100.0f // 최대 값 + +const val DEFAULT_Q_SWITCH_VALUE = 000.0f +const val MIN_Q_SWITCH_VALUE = 000.0f +const val MAX_Q_SWITCH_VALUE = 250.0f + +const val MAX_REFER1_VALUE = 99999999 // 8 byte ascii (ENERGY DETECT MEASURED VALUE) +const val DEFAULT_REFER1_VALUE = 0 // initial state + +const val DEFAULT_DCD_GAS_PRESSURE = 06.5f +const val DEFAULT_SPAY_DCD_TIME = 10 +const val DEFAULT_SPAY_DCD_DELAY = 10 + +const val MAX_LOG_COUNT = 259200 // 약 1개월 정도 (30일/5초 * 3600 * 12) + +const val RX_TIMEOUT_THRESHOLD = 5000L // 5 초 + +object CMD { + const val VERSION = 0x01 + const val LASER_STATUS = 0x03 + const val PURGE_BUBBLE = 0x05 + const val DCD_GAS = 0x06 + const val SPRAY_DCD = 0x07 + const val HAND_PIECE = 0x08 + const val GUIDE_BEAM = 0x09 + const val HP_SHOT_COUNT = 0x0A + const val ENERGE_METER = 0x0B + const val TEMPERATURE = 0x0C + const val OVEN = 0x0D + const val Q_SWITCH = 0x0F + const val ERROR = 0x10 + const val WARNING = 0x11 + const val ENERGY_DETECT = 0x14 + const val UPDATE_FIRMWARE = 0x20 + const val CMD_FLAG = 0x80 + const val WRITE_FLAG = 0x40 + const val STX = 0x21 + const val ETX = 0x0D + const val TIMEOUT = 0x01F4 // 500 +} + +object LASER_STATUS { + const val STAND_BY = 0x53 // (S)tandBy + const val READY = 0x52 // (R)eady + const val LASER_ON = 0x4C // (L)aserOn + const val INTERVAL = 0x49 // (I)nterval +} + +object PURGE_STATUS { + const val ACCEPT = 0x41 // Activate (or Accept) + const val COMPLTE = 0x42 // Completed + const val FAIL = 0x46 // Defuse + const val INIT = 0x00 +} + +object DCD_STATUS { + const val ON = 0x41 // A + const val OFF = 0x44 // D + const val PRESSURE = 0x50 +} + +object SPRAY_STATUS { + const val ON = 0x41 // A + const val OFF = 0x44 // D + const val PRESSURE = 0x50 +} + +enum class READ_WRITE { + READ, WRITE, NONE +} + +enum class TX_RX { + TX, RX, NONE +} + +enum class AlertType { + WARNING, + ERROR +} + + +val ERROR_TABLE: List = listOf ( + R.string.err_none, // 00 + R.string.err_ps_comm, + R.string.err_head_comm, + R.string.err_simmer, + R.string.err_charge, + R.string.err_discharge, + R.string.err_optical_system, + R.string.err_water_level, + R.string.err_water_flow, + R.string.err_e_detect_system, + R.string.err_ktp_temp, // 10 + R.string.err_chamber_temp, + R.string.err_baseplate_temp, + R.string.err_water_temp, + R.string.err_dcd_temp, + R.string.err_dcd_pressure, + R.string.err_shutter1, + R.string.err_shutter2, + R.string.err_not_defined, + R.string.err_interlock, + R.string.err_other, // 20 +) + +val WARNING_TABLE: List = listOf ( + R.string.warn_none, // 00 + R.string.warn_water_level, + R.string.warn_optical_energy, + R.string.warn_dcd_gas_shortage, + R.string.warn_dcd_bubble, + R.string.warn_internal_temp_high, + R.string.warn_internal_temp_low, + R.string.warn_humidity_1, + R.string.warn_humidity_2, + R.string.warn_hp_tip, + R.string.warn_lamp_count, // 10 + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, + R.string.warn_not_defined, // 20 +) + +val handPieceTypes = listOf ( + "NONE", + "5x5", + "7x7", + "10x10", + "12x12", + "3x15", + "NONE", + "NONE", +) +val validHandPieceTypes = handPieceTypes.filter { it != "NONE" } + +val lifeTimeTypes = listOf ( + "Lamp", + "HP 5x5", + "HP 7x7", + "HP 10x10", + "HP 12x12", + "HP 3x15", + "Detector", + "Water", +) + +val temperatureTypes = listOf ( + "KTP", + "Chamber 1", + "Chamber 2", + "Baseplate", + "Water", + "Reserved", + "Reserved", + "Reserved", +) + +// PulseDurations (= Pulse Width): xx.xf (ms), 12단계 +val PulseDurations: List = listOf ( + 0.5f, + 1.0f, + 1.5f, + 3.0f, + 5.0f, + 10.0f, + 15.0f, + 20.0f, + 25.0f, + 30.0f, + 35.0f, + 40.0f +) + +// Fluences (= Energy): xxx (J/cm²), +// FluenceTable_0 (PulseWith: 0.5) +val Fluences: List = listOf ( + 2.0f, + 2.1f, + 2.2f, + 2.3f, + 2.4f, + 2.5f, + 2.6f, + 2.7f, + 2.8f, + 2.9f, + 3.0f, + 3.2f, + 3.4f, + 3.6f, + 3.8f, + 4.0f, + 4.2f, + 4.4f, + 4.6f, + 4.8f, + 5.0f, + 5.5f, + 6.0f, + 6.5f, + 7.0f, + 7.5f, + 8.0f, +) + +// Energy (J) - Fixed Energy List +val Energies: List = listOf ( + 00.5f, + 01.0f, + 02.0f, + 03.0f, + 04.0f, + 05.0f, + 06.0f, + 07.0f, + 08.0f, + 09.0f, + 10.0f +) + +// Repetitions (= Frequency): xx.xf (Hz) +val Repetitions: List = listOf ( + 01.0f, + 02.0f, + 03.0f, + 04.0f, + 05.0f, + 06.0f, + 07.0f, + 08.0f, + 09.0f, + 10.0f +) + +// Define integer keys for your lists +// You can choose any integers that make sense for your application +const val KEY_GREEN = 0 +const val KEY_BLUE = 1 +const val KEY_YELLOW = 2 +const val KEY_RED = 3 +const val KEY_GRAY = 4 + +// Combined map +// eg. get RepetitionList by Key. +// val repetitionList: List? = RepetitionsByColorKey[KEY_GRAY] +val RepetitionsByColorKey: Map> = mapOf( + KEY_GREEN to listOf ( + 01.0f, + 02.0f, + 03.0f, + 04.0f, + 05.0f, + 06.0f, + 07.0f, + 08.0f, + 09.0f, + 10.0f + ), + KEY_BLUE to listOf ( + 01.0f, + 02.0f, + 03.0f, + 04.0f, + 05.0f, + ), + KEY_YELLOW to listOf ( + 01.0f, + 02.0f, + 03.0f, + ), + KEY_RED to listOf ( + 01.0f, + 02.0f, + ), + KEY_GRAY to listOf ( + 01.0f, + ) +) + +enum class LaserParameter { + PulseWidth, + Fluence, + Repetition, +} + +enum class UpDownState { + Up, + Down +} + +enum class GASCanValueState { + On, + Off +} + +enum class PurgeState { + On, + Off, + Disabled +} + +enum class GasSettingState { + On, + Off +} + +enum class MinMaxUpDownState { + MinUp, + MinDown, + MaxUp, + MaxDown, + MinLongUp, + MinLongDown, + MaxLongUp, + MaxLongDown, +} + +enum class LaserStatusType { + PULSE_DURATION, + FLUENCE, + REPETITION, + SKIN_TEMP, +} + +enum class PresetButtonType { + SAVE, + LOAD, +} + +val engineerModeButtonLists: List = listOf( + IconButtonItem( + title = R.string.update_title, + image = R.drawable.ic_update, + containerColor = Color(26,51,81), + contentColor = Color(255,255,255) + ), + IconButtonItem( + title = R.string.default_title, + image = R.drawable.ic_default, + containerColor = Color(36,67,105), + contentColor = Color(255,255,255) + ), + IconButtonItem( + title = R.string.save_title, + image = R.drawable.ic_save, + containerColor = Color(35,35,33), + contentColor = Color(243,205,81) + ), + IconButtonItem( + title = R.string.load_title, + image = R.drawable.ic_load, + containerColor = Color(46,103,59), + contentColor = Color(255,255,255) + ), + IconButtonItem( + title = R.string.import_title, + image = R.drawable.ic_import, + containerColor = Color(47,51,57), + contentColor = Color(255,255,255) + ), + IconButtonItem( + title = R.string.export_title, + image = R.drawable.ic_export, + containerColor = Color(47,51,57), + contentColor = Color(255,255,255) + ), + IconButtonItem( + title = R.string.log_title, + image = R.drawable.ic_log, + containerColor = Color(255,255,255), + contentColor = Color(0,0,0) + ), +) + +// Product Serial Number +// eg. LO-3ND-BU-OCL-D +// LO : 회사 약호 +// 3ND : 제품 기호 +// BU : 제조년도 (끝2자리) +// OCL: 생산수량 +// D : 제조월 +// 제조연도 (십의자리) 또는 생산수량 (십의자리) +private val productYearOrQuantityTenthRange = 'A' .. 'J' +val productYearOrQuantityTenthRangeList = productYearOrQuantityTenthRange.toList().map{it.toString()} + +// 제조연도 (일의자리) +private val productYearOrQuantityOnethRange = 'L' .. 'U' +val productYearOrQuantityOnethRangeList = productYearOrQuantityOnethRange.toList().map{it.toString()} + +// 생산수량 (백의 자리) +private val productQuantityHundredthRange = '0' .. '9' +val productQuantityHundredthRangeList = productQuantityHundredthRange.toList().map{it.toString()} + +// 제조월 (1월 ~ 12월) +val productMonthLists = listOf("L", "A", "S", "E", "R", "O", "P", "T", "2", "K", "N", "D") + +val dcdCanTypeLists = listOf(350, 700) // listOf( 100, 300, 500, 700, 1000) +val dcdCanTypeFormatedLists = dcdCanTypeLists.map{ it.formatWithThousandComma() + " ml" } + +val dcdLifeSpanAdjustLists = listOf( 0.5f, 0.7f, 0.9f, 1.0f, 1.1f, 1.2f ) +val dcdLifeSpanAdjustStringLists = dcdLifeSpanAdjustLists.map{ it.toString() } + +val dcdSprayValues = listOf(10, 20, 30, 40, 50, 60, 70, 80, 90, 100).map{ it.toString() } +val dcdDelayValues = listOf(10, 20, 30, 40, 50, 60, 70, 80, 90, 100).map{ it.toString() } + +// default spray dcd list for options +val SprayDcdList = listOf( + SprayDcd(status = 0x44, sprayTime = 10, sprayDelay = 10), // default + SprayDcd(status = 0x44, sprayTime = 10, sprayDelay = 10), + SprayDcd(status = 0x44, sprayTime = 20, sprayDelay = 20), + SprayDcd(status = 0x44, sprayTime = 30, sprayDelay = 20), + SprayDcd(status = 0x44, sprayTime = 40, sprayDelay = 40), + SprayDcd(status = 0x44, sprayTime = 50, sprayDelay = 50), + SprayDcd(status = 0x44, sprayTime = 60, sprayDelay = 60), +) + +val PresetList = listOf( + /* + Preset(id = 0, handPieceType = 0, pulseWidth = 0.5f, fluence = 2.0f, repetition = 1.0f, priority = 0, name = "00"), + Preset(id = 1, handPieceType = 0, pulseWidth = 0.5f, fluence = 2.0f, repetition = 1.0f, priority = 1, name = "01"), + Preset(id = 2, handPieceType = 0, pulseWidth = 0.5f, fluence = 2.0f, repetition = 1.0f, priority = 2, name = "02"), + Preset(id = 3, handPieceType = 0, pulseWidth = 0.5f, fluence = 2.0f, repetition = 1.0f, priority = 3, name = "03"), + Preset(id = 4, handPieceType = 0, pulseWidth = 0.5f, fluence = 2.0f, repetition = 1.0f, priority = 4, name = "04"), + Preset(id = 5, handPieceType = 0, pulseWidth = 0.5f, fluence = 2.0f, repetition = 1.0f, priority = 5, name = "05"), + ... + */ +) + + + diff --git a/app/src/main/java/com/laseroptek/raman/const/VoltageTable.kt b/app/src/main/java/com/laseroptek/raman/const/VoltageTable.kt new file mode 100644 index 0000000..58c745e --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/const/VoltageTable.kt @@ -0,0 +1,181 @@ +package com.laseroptek.raman.const + +// +// Voltage Table +// - Map, Int> +// - Map, VALUE> +// - Map, Voltage(V)> +// > PulseWidth = 0.5, 1.0, 1.5, 3.0, 4.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0 (13 단계) +// > Energy = 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0 (11 단계) + +val VoltageTable = mapOf( + // Pulse Width = 0.5f + Pair(0.5f, 00.5f) to 440, + Pair(0.5f, 01.0f) to 441, + Pair(0.5f, 02.0f) to 442, + Pair(0.5f, 03.0f) to 443, + Pair(0.5f, 04.0f) to 444, + Pair(0.5f, 05.0f) to 445, + Pair(0.5f, 06.0f) to 446, + Pair(0.5f, 07.0f) to 447, + Pair(0.5f, 08.0f) to 448, + Pair(0.5f, 09.0f) to 449, + Pair(0.5f, 10.0f) to 450, + + // Pulse Width = 1.0f + Pair(1.0f, 00.5f) to 441, + Pair(1.0f, 01.0f) to 442, + Pair(1.0f, 02.0f) to 443, + Pair(1.0f, 03.0f) to 444, + Pair(1.0f, 04.0f) to 445, + Pair(1.0f, 05.0f) to 446, + Pair(1.0f, 06.0f) to 447, + Pair(1.0f, 07.0f) to 448, + Pair(1.0f, 08.0f) to 449, + Pair(1.0f, 09.0f) to 450, + Pair(1.0f, 10.0f) to 451, + + // Pulse Width = 1.5f + Pair(1.5f, 00.5f) to 442, + Pair(1.5f, 01.0f) to 443, + Pair(1.5f, 02.0f) to 444, + Pair(1.5f, 03.0f) to 445, + Pair(1.5f, 04.0f) to 446, + Pair(1.5f, 05.0f) to 447, + Pair(1.5f, 06.0f) to 458, + Pair(1.5f, 07.0f) to 459, + Pair(1.5f, 08.0f) to 450, + Pair(1.5f, 09.0f) to 451, + Pair(1.5f, 10.0f) to 452, + + // Pulse Width = 3f + Pair(3.0f, 00.5f) to 443, + Pair(3.0f, 01.0f) to 444, + Pair(3.0f, 02.0f) to 445, + Pair(3.0f, 03.0f) to 446, + Pair(3.0f, 04.0f) to 447, + Pair(3.0f, 05.0f) to 448, + Pair(3.0f, 06.0f) to 449, + Pair(3.0f, 07.0f) to 450, + Pair(3.0f, 08.0f) to 453, + Pair(3.0f, 09.0f) to 453, + Pair(3.0f, 10.0f) to 453, + + // Pulse Width = 4f + Pair(4.0f, 00.5f) to 444, + Pair(4.0f, 01.0f) to 445, + Pair(4.0f, 02.0f) to 446, + Pair(4.0f, 03.0f) to 447, + Pair(4.0f, 04.0f) to 448, + Pair(4.0f, 05.0f) to 449, + Pair(4.0f, 06.0f) to 450, + Pair(4.0f, 07.0f) to 451, + Pair(4.0f, 08.0f) to 452, + Pair(4.0f, 09.0f) to 453, + Pair(4.0f, 10.0f) to 454, + + // Pulse Width = 5f + Pair(5.0f, 00.5f) to 445, + Pair(5.0f, 01.0f) to 446, + Pair(5.0f, 02.0f) to 447, + Pair(5.0f, 03.0f) to 448, + Pair(5.0f, 04.0f) to 449, + Pair(5.0f, 05.0f) to 450, + Pair(5.0f, 06.0f) to 451, + Pair(5.0f, 07.0f) to 452, + Pair(5.0f, 08.0f) to 453, + Pair(5.0f, 09.0f) to 454, + Pair(5.0f, 10.0f) to 455, + + // Pulse Width = 10f + Pair(10.0f, 00.5f) to 446, + Pair(10.0f, 01.0f) to 447, + Pair(10.0f, 02.0f) to 448, + Pair(10.0f, 03.0f) to 449, + Pair(10.0f, 04.0f) to 450, + Pair(10.0f, 05.0f) to 451, + Pair(10.0f, 06.0f) to 452, + Pair(10.0f, 07.0f) to 453, + Pair(10.0f, 08.0f) to 454, + Pair(10.0f, 09.0f) to 455, + Pair(10.0f, 10.0f) to 456, + + // Pulse Width = 15f + Pair(15.0f, 00.5f) to 446, + Pair(15.0f, 01.0f) to 447, + Pair(15.0f, 02.0f) to 448, + Pair(15.0f, 03.0f) to 449, + Pair(15.0f, 04.0f) to 450, + Pair(15.0f, 05.0f) to 451, + Pair(15.0f, 06.0f) to 452, + Pair(15.0f, 07.0f) to 453, + Pair(15.0f, 08.0f) to 454, + Pair(15.0f, 09.0f) to 455, + Pair(15.0f, 10.0f) to 456, + + // Pulse Width = 20f + Pair(20.0f, 00.5f) to 446, + Pair(20.0f, 01.0f) to 447, + Pair(20.0f, 02.0f) to 448, + Pair(20.0f, 03.0f) to 449, + Pair(20.0f, 04.0f) to 450, + Pair(20.0f, 05.0f) to 451, + Pair(20.0f, 06.0f) to 452, + Pair(20.0f, 07.0f) to 453, + Pair(20.0f, 08.0f) to 454, + Pair(20.0f, 09.0f) to 455, + Pair(20.0f, 10.0f) to 456, + + // Pulse Width = 25f + Pair(25.0f, 00.5f) to 446, + Pair(25.0f, 01.0f) to 447, + Pair(25.0f, 02.0f) to 448, + Pair(25.0f, 03.0f) to 449, + Pair(25.0f, 04.0f) to 450, + Pair(25.0f, 05.0f) to 451, + Pair(25.0f, 06.0f) to 452, + Pair(25.0f, 07.0f) to 453, + Pair(25.0f, 08.0f) to 454, + Pair(25.0f, 09.0f) to 455, + Pair(25.0f, 10.0f) to 456, + + // Pulse Width = 30f + Pair(30.0f, 00.5f) to 446, + Pair(30.0f, 01.0f) to 447, + Pair(30.0f, 02.0f) to 448, + Pair(30.0f, 03.0f) to 449, + Pair(30.0f, 04.0f) to 450, + Pair(30.0f, 05.0f) to 451, + Pair(30.0f, 06.0f) to 452, + Pair(30.0f, 07.0f) to 453, + Pair(30.0f, 08.0f) to 454, + Pair(30.0f, 09.0f) to 455, + Pair(30.0f, 10.0f) to 456, + + // Pulse Width = 35f + Pair(35.0f, 00.5f) to 446, + Pair(35.0f, 01.0f) to 447, + Pair(35.0f, 02.0f) to 448, + Pair(35.0f, 03.0f) to 449, + Pair(35.0f, 04.0f) to 450, + Pair(35.0f, 05.0f) to 451, + Pair(35.0f, 06.0f) to 452, + Pair(35.0f, 07.0f) to 453, + Pair(35.0f, 08.0f) to 454, + Pair(35.0f, 09.0f) to 455, + Pair(35.0f, 10.0f) to 456, + + // Pulse Width = 40f + Pair(40.0f, 00.5f) to 446, + Pair(40.0f, 01.0f) to 447, + Pair(40.0f, 02.0f) to 448, + Pair(40.0f, 03.0f) to 449, + Pair(40.0f, 04.0f) to 450, + Pair(40.0f, 05.0f) to 451, + Pair(40.0f, 06.0f) to 452, + Pair(40.0f, 07.0f) to 453, + Pair(40.0f, 08.0f) to 454, + Pair(40.0f, 09.0f) to 455, + Pair(40.0f, 10.0f) to 456 +) + diff --git a/app/src/main/java/com/laseroptek/raman/data/model/CountType.kt b/app/src/main/java/com/laseroptek/raman/data/model/CountType.kt new file mode 100644 index 0000000..6a45d32 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/CountType.kt @@ -0,0 +1,16 @@ +package com.laseroptek.raman.data.model + +enum class CountType { + LASER, + LAMP, + DCD; +} + +/* +println("Index of CountType.LASER: ${CountType.LASER.index}") // Output: Index of CountType.LASER: 0 + println("Index of CountType.LAMP: ${CountType.LAMP.index}") // Output: Index of CountType.LAMP: 1 + println("Index of CountType.DCD: ${CountType.DCD.index}") // Output: Index of CountType.DCD: 2 + + // Or, if you prefer to use ordinal directly: + println("Ordinal of CountType.LAMP: ${CountType.LAMP.ordinal}") // Output: Ordinal of CountType.LAMP: 1 + */ \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/DcdType.kt b/app/src/main/java/com/laseroptek/raman/data/model/DcdType.kt new file mode 100644 index 0000000..1e8b558 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/DcdType.kt @@ -0,0 +1,6 @@ +package com.laseroptek.raman.data.model + +data class DcdType( + var canType: Int = 350, // 700 ml (fixed), 1,000 ml + var lifeSpan: Float = 1.0f, // Lifespan Adjust value +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/FluenceEntry.kt b/app/src/main/java/com/laseroptek/raman/data/model/FluenceEntry.kt new file mode 100644 index 0000000..056cec9 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/FluenceEntry.kt @@ -0,0 +1,4 @@ +package com.laseroptek.raman.data.model + +// Energy TableEntry : Preference save format +data class FluenceEntry(val pulse: Float, val fluence: Float, val energy: Float) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/IconButtonItem.kt b/app/src/main/java/com/laseroptek/raman/data/model/IconButtonItem.kt new file mode 100644 index 0000000..337bda6 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/IconButtonItem.kt @@ -0,0 +1,12 @@ +package com.laseroptek.raman.data.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.Color + +data class IconButtonItem ( + @param:StringRes val title: Int, + @param:DrawableRes val image: Int, + val containerColor: Color, // background color + val contentColor: Color // icon and text color +) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/LifeTime.kt b/app/src/main/java/com/laseroptek/raman/data/model/LifeTime.kt new file mode 100644 index 0000000..41e5300 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/LifeTime.kt @@ -0,0 +1,17 @@ +package com.laseroptek.raman.data.model + +import com.laseroptek.raman.const.DEFAULT_LIFETIME_DETECTOR +import com.laseroptek.raman.const.DEFAULT_LIFETIME_HP +import com.laseroptek.raman.const.DEFAULT_LIFETIME_LAMP +import com.laseroptek.raman.const.DEFAULT_LIFETIME_WATER + +data class LifeTime( + val lamp: Int = DEFAULT_LIFETIME_LAMP, + val hp5x5: Int = DEFAULT_LIFETIME_HP, + val hp7x7: Int = DEFAULT_LIFETIME_HP, + val hp10x10: Int = DEFAULT_LIFETIME_HP, + val hp12x12: Int = DEFAULT_LIFETIME_HP, + val hp3x15: Int = DEFAULT_LIFETIME_HP, + val detector: Int = DEFAULT_LIFETIME_DETECTOR, + val water: Int = DEFAULT_LIFETIME_WATER, +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/Preset.kt b/app/src/main/java/com/laseroptek/raman/data/model/Preset.kt new file mode 100644 index 0000000..7ec3f19 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/Preset.kt @@ -0,0 +1,26 @@ +package com.laseroptek.raman.data.model + +import com.laseroptek.raman.const.Fluences +import com.laseroptek.raman.const.PulseDurations + +// Preset to Voltage of LaserStatus +data class Preset( + var pulseWidth: Float = PulseDurations[0], + var fluence: Float = Fluences[0], + var repetition: Float = 1f, + + var name: String = "", + var priority: Int = 0, // 0: none, 1:1, 2:2, 3:3, 4:4, 5:5, 6:6, + var handPieceType: Int = 0, + + var id: Int = 0 // seq no 추가 시 max id + 1 +) + +/* cf) +data class LaserStatus( + var laserStatus: Int = 0x53, // 0x53(Standby) or 0x52(Ready) or 0x4C(Laser on) or 0x49(Interval) + var voltage: Int = 0, // ascii 3 byte (PulseWidth() ms, Energy/Fluence() J/cm² : 13 단계 ) -> Votage + var onTime: Float = 00.0f, // xx.x (Pulse PulseWidth? 0.5, 1, 1.5, 4, 5, 10 .. 40 ms : 12 단계) + var frequence: Float = 00.0f // xx.x (Repetition? 1, 1.5, 2 10hz : 10 단계) +) +*/ diff --git a/app/src/main/java/com/laseroptek/raman/data/model/Refer2.kt b/app/src/main/java/com/laseroptek/raman/data/model/Refer2.kt new file mode 100644 index 0000000..e98de53 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/Refer2.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.data.model + +import com.laseroptek.raman.const.DEFAULT_REFER2_ACCEPTABLE +import com.laseroptek.raman.const.DEFAULT_REFER2_BAD +import com.laseroptek.raman.const.DEFAULT_REFER2_GOOD + +data class Refer2( + var good: Float = DEFAULT_REFER2_GOOD, // Good - default - 10% + var acceptable: Float = DEFAULT_REFER2_ACCEPTABLE, // Acceptable - default 25% + var bad: Float = DEFAULT_REFER2_BAD, // Bad - default 25% +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/RepetitionEntry.kt b/app/src/main/java/com/laseroptek/raman/data/model/RepetitionEntry.kt new file mode 100644 index 0000000..d8b4ab6 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/RepetitionEntry.kt @@ -0,0 +1,5 @@ +package com.laseroptek.raman.data.model + +// Repetition TableEntry : Preference save format +// repetition: (0: Green 6~10Hz, 1: Blue 4~5Hz, 2: Yellow: 3Hz, 3: Red: 2Hz, 4: grAy: 1Hz) +data class RepetitionEntry(val pulse: Float, val fluence: Float, val hzType: Int) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/SerialNumber.kt b/app/src/main/java/com/laseroptek/raman/data/model/SerialNumber.kt new file mode 100644 index 0000000..cbdf1f9 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/SerialNumber.kt @@ -0,0 +1,10 @@ +package com.laseroptek.raman.data.model + +data class SerialNumber ( + val char1: Char = 'B', + val char2: Char = 'M', + val char3: Char = '1', + val char4: Char = 'B', + val char5: Char = 'B', + val char6: Char = 'S', +) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/SerialResult.kt b/app/src/main/java/com/laseroptek/raman/data/model/SerialResult.kt new file mode 100644 index 0000000..caa6684 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/SerialResult.kt @@ -0,0 +1,14 @@ +package com.laseroptek.raman.data.model + +/** + * A generic class that holds a value or an exception + */ +sealed class SerialResult { + data class Success(val data: T) : SerialResult() + data class Error(val exception: Exception) : SerialResult() + //data class Loading(val isLoading: Boolean) : SerialResult() +} + +fun SerialResult.successOr(fallback: T): T { + return (this as? SerialResult.Success)?.data ?: fallback +} diff --git a/app/src/main/java/com/laseroptek/raman/data/model/VoltageEntry.kt b/app/src/main/java/com/laseroptek/raman/data/model/VoltageEntry.kt new file mode 100644 index 0000000..3507580 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/VoltageEntry.kt @@ -0,0 +1,4 @@ +package com.laseroptek.raman.data.model + +// Voltage Table Entry : Preference save format +data class VoltageEntry(val pulse: Float, val energy: Float, val voltage: Int) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/AnimationMode.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/AnimationMode.kt new file mode 100644 index 0000000..4f5c9e4 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/AnimationMode.kt @@ -0,0 +1,6 @@ +package com.laseroptek.raman.data.model.chart + +sealed class AnimationMode { + data class Together(val delayBuilder: (index: Int) -> Long = { 0 }) : AnimationMode() + data object OneByOne : AnimationMode() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/BarProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/BarProperties.kt new file mode 100644 index 0000000..6c69212 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/BarProperties.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class BarProperties( + val thickness: Dp = 15.dp, + val spacing: Dp = 6.dp, + val cornerRadius: Bars.Data.Radius = Bars.Data.Radius.None, + val style: DrawStyle = DrawStyle.Fill +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/Bars.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/Bars.kt new file mode 100644 index 0000000..72f16a6 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/Bars.kt @@ -0,0 +1,61 @@ +package com.laseroptek.raman.data.model.chart + + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.random.Random + +data class Bars( + val label: String, + val values: List +) { + data class Data( + val id:Int = Random.nextInt(0, 999999), + val label: String? = null, + val value: Double, + val color: Brush, + val properties: BarProperties? = null, + val animationSpec: AnimationSpec? = null, + val animator:Animatable = Animatable(0f) + ) { + sealed class Radius() { + data object None : Radius() + data class Circular(val radius: Dp) : Radius() + data class Rectangle( + val topLeft: Dp = 0.dp, + val topRight: Dp = 0.dp, + val bottomLeft: Dp = 0.dp, + val bottomRight: Dp = 0.dp + ) : Radius() + + fun reverse(horizontal:Boolean = false):Radius{ + return when(this){ + is Circular, is None->{ + this + } + is Rectangle->{ + if (horizontal){ + copy( + topLeft = topRight, + topRight = topLeft, + bottomLeft = bottomRight, + bottomRight = bottomLeft + ) + }else{ + copy( + topLeft = bottomLeft, + topRight = bottomRight, + bottomLeft = topLeft, + bottomRight = topRight + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/DividerProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/DividerProperties.kt new file mode 100644 index 0000000..ee37ecb --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/DividerProperties.kt @@ -0,0 +1,7 @@ +package com.laseroptek.raman.data.model.chart + +data class DividerProperties( + val enabled:Boolean = true, + val xAxisProperties:LineProperties = LineProperties(), + val yAxisProperties:LineProperties = LineProperties() +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/DotProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/DotProperties.kt new file mode 100644 index 0000000..a68e409 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/DotProperties.kt @@ -0,0 +1,20 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class DotProperties( + val enabled: Boolean = false, + val radius: Dp = 3.dp, + val color: Brush = SolidColor(Color(0xffE1E2EC)), + val strokeWidth: Dp = 2.dp, + val strokeColor: Brush = SolidColor(Color.Unspecified), + val strokeStyle: StrokeStyle = StrokeStyle.Normal, + val animationEnabled:Boolean = true, + val animationSpec: AnimationSpec = tween(300) +) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/DrawStyle.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/DrawStyle.kt new file mode 100644 index 0000000..5ddfa79 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/DrawStyle.kt @@ -0,0 +1,28 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.graphics.drawscope.DrawStyle as CanvasDrawStyle + +sealed class DrawStyle() { + data class Stroke(val width: Dp = 2.dp, val strokeStyle: StrokeStyle = StrokeStyle.Normal) : + DrawStyle() + + + data object Fill : DrawStyle() + + fun getStyle(density: Float):CanvasDrawStyle{ + return when(this){ + is Stroke->{ + return Stroke( + width = this.width.value*density, + pathEffect = this.strokeStyle.pathEffect + ) + } + is Fill->{ + return androidx.compose.ui.graphics.drawscope.Fill + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/GridProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/GridProperties.kt new file mode 100644 index 0000000..dbb6f09 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/GridProperties.kt @@ -0,0 +1,22 @@ +package com.laseroptek.raman.data.model.chart + + +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class GridProperties( + val enabled: Boolean = true, + val xAxisProperties: AxisProperties = AxisProperties(), + val yAxisProperties: AxisProperties = AxisProperties() +){ + data class AxisProperties( + val enabled: Boolean = true, + val style: StrokeStyle = StrokeStyle.Normal, + val color: Brush = SolidColor(Color.Gray), + val thickness: Dp = (.5).dp, + val lineCount:Int = 5 + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/IndicatorProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/IndicatorProperties.kt new file mode 100644 index 0000000..39c9acc --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/IndicatorProperties.kt @@ -0,0 +1,71 @@ +package com.laseroptek.raman.data.model.chart + + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.utils.ext.format + + +sealed class IndicatorProperties( + open val enabled:Boolean, + open val textStyle: TextStyle, + open val count: IndicatorCount, + open val position: IndicatorPosition, + open val padding: Dp, + open val contentBuilder: (Double) -> String +) + + +data class VerticalIndicatorProperties( + override val enabled: Boolean = true, + override val textStyle: TextStyle = TextStyle.Default.copy(fontSize = 12.sp), + override val count: IndicatorCount = IndicatorCount.CountBased(count = 5), + override val position: IndicatorPosition.Vertical = IndicatorPosition.Vertical.Bottom, + override val padding: Dp = 12.dp, + override val contentBuilder: (Double) -> String = { + it.format(1) + } +) : IndicatorProperties( + enabled = enabled, + textStyle = textStyle, + count = count, + position = position, + contentBuilder = contentBuilder, + padding = padding +) + +data class HorizontalIndicatorProperties( + override val enabled: Boolean = false, + override val textStyle: TextStyle = TextStyle.Default.copy(fontSize = 12.sp), + override val count: IndicatorCount = IndicatorCount.CountBased(count = 5), + override val position: IndicatorPosition.Horizontal = IndicatorPosition.Horizontal.Start, + override val padding: Dp = 12.dp, + override val contentBuilder: (Double) -> String = { + it.format(1) + } +) : IndicatorProperties( + enabled = enabled, + textStyle = textStyle, + count = count, + position = position, + contentBuilder = contentBuilder, + padding = padding +) + +sealed interface IndicatorPosition { + enum class Vertical : IndicatorPosition { + Top, + Bottom + } + enum class Horizontal: IndicatorPosition { + Start, + End + } +} + +sealed class IndicatorCount { + data class CountBased(val count: Int) : IndicatorCount() + data class StepBased(val stepBy: Double) : IndicatorCount() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/LInePropertes.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/LInePropertes.kt new file mode 100644 index 0000000..47d3680 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/LInePropertes.kt @@ -0,0 +1,15 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class LineProperties( + val enabled: Boolean = true, + val style: StrokeStyle = StrokeStyle.Normal, + val color: Brush = SolidColor(Color.Gray), + val thickness: Dp = (.5).dp, +) + diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/LabelHelperProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/LabelHelperProperties.kt new file mode 100644 index 0000000..4a26980 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/LabelHelperProperties.kt @@ -0,0 +1,9 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp + +data class LabelHelperProperties( + val enabled:Boolean = false, + val textStyle: TextStyle = TextStyle.Default.copy(fontSize = 8.sp) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/LabelProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/LabelProperties.kt new file mode 100644 index 0000000..1675b93 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/LabelProperties.kt @@ -0,0 +1,16 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +data class LabelProperties( + val enabled:Boolean = true, + val textStyle: TextStyle = TextStyle.Default.copy(fontSize = 12.sp, textAlign = TextAlign.End), + val padding:Dp = 12.dp, + val labels: List = listOf(), + val rotationDegreeOnSizeConflict: Float = -45f, + val forceRotation: Boolean = false, +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/Line.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/Line.kt new file mode 100644 index 0000000..9f5e1ca --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/Line.kt @@ -0,0 +1,33 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.EaseInOutCubic +import androidx.compose.animation.core.tween +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.dp + +data class Line( + val label: String, + var values: List, + val color: Brush, + val firstGradientFillColor: Color? = null, + val secondGradientFillColor: Color? = null, + val drawStyle: DrawStyle = DrawStyle.Stroke(2.dp), + val strokeAnimationSpec: AnimationSpec = tween(2000, easing = EaseInOutCubic), + val gradientAnimationSpec: AnimationSpec = tween(2000), + val gradientAnimationDelay: Long = 1000, + val dotProperties: DotProperties? = DotProperties( + enabled = true, + color = SolidColor(Color(0xffE1E2EC)), + strokeWidth = 2.dp, + strokeColor = SolidColor(Color(0, 204, 204)), + ), + val popupProperties: PopupProperties? = null, + val curvedEdges:Boolean? = true, + val strokeProgress: Animatable = Animatable(0f), + val gradientProgress: Animatable = Animatable(0f), +) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/PopupProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/PopupProperties.kt new file mode 100644 index 0000000..49dbdba --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/PopupProperties.kt @@ -0,0 +1,24 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.utils.ext.format + +data class PopupProperties( + val enabled: Boolean = false, + val animationSpec: AnimationSpec = tween(400), + val duration: Long = 1500, + val textStyle: TextStyle = TextStyle.Default.copy(fontSize = 12.sp), + val containerColor: Color = Color(0xff313131), + val cornerRadius: Dp = 6.dp, + val contentHorizontalPadding: Dp = 4.dp, + val contentVerticalPadding: Dp = 2.dp, + val contentBuilder: (value: Double) -> String = { + it.format(1) + } +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/StrokeStyle.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/StrokeStyle.kt new file mode 100644 index 0000000..a73251e --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/StrokeStyle.kt @@ -0,0 +1,37 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.ui.graphics.PathEffect + + +sealed class StrokeStyle{ + data object Normal:StrokeStyle() + data class Dashed(val intervals:FloatArray = floatArrayOf(10f,10f), val phase:Float = 10f):StrokeStyle() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + other as Dashed + + if (!intervals.contentEquals(other.intervals)) return false + if (phase != other.phase) return false + + return true + } + + override fun hashCode(): Int { + var result = intervals.contentHashCode() + result = 31 * result + phase.hashCode() + return result + } + } + + val pathEffect:PathEffect? get() { + return when(this){ + is Normal->{ + null + } + is Dashed->{ + PathEffect.dashPathEffect(intervals = intervals, phase = phase) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/chart/ZeroLineProperties.kt b/app/src/main/java/com/laseroptek/raman/data/model/chart/ZeroLineProperties.kt new file mode 100644 index 0000000..cf125c5 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/chart/ZeroLineProperties.kt @@ -0,0 +1,22 @@ +package com.laseroptek.raman.data.model.chart + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class ZeroLineProperties( + val enabled: Boolean = false, + val style: StrokeStyle = StrokeStyle.Normal, + val color: Brush = SolidColor(Color.Gray), + val thickness: Dp = (.5).dp, + val animationSpec:AnimationSpec = tween(durationMillis = 1000, delayMillis = 300), + val zType: ZType = ZType.Under +){ + enum class ZType{ + Under,Above + } +} diff --git a/app/src/main/java/com/laseroptek/raman/data/model/engineer/LaserCount.kt b/app/src/main/java/com/laseroptek/raman/data/model/engineer/LaserCount.kt new file mode 100644 index 0000000..79ae8b8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/engineer/LaserCount.kt @@ -0,0 +1,3 @@ +package com.laseroptek.raman.data.model.engineer + +data class LaserCount (val count : Int) \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/Alert.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/Alert.kt new file mode 100644 index 0000000..c149b7f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/Alert.kt @@ -0,0 +1,5 @@ +package com.laseroptek.raman.data.model.serial + +data class Alert( + val code: Int = 0, +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/DcdGas.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/DcdGas.kt new file mode 100644 index 0000000..e6eb259 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/DcdGas.kt @@ -0,0 +1,9 @@ +package com.laseroptek.raman.data.model.serial + +import com.laseroptek.raman.const.DEFAULT_DCD_GAS_PRESSURE + +data class DcdGas( + val status: Int = 0x41, // 1Byte(A(0x41): on, D(0x44): off, P(0x50): pressure) + val pressure: Float = DEFAULT_DCD_GAS_PRESSURE, // Ascii 4Byte (xx.x): Pressure + val ok: Int = 0x00, // 1Byte(N(0x4E): Not OK, O(0x00F): OK)/ +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/DcdGasOnOff.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/DcdGasOnOff.kt new file mode 100644 index 0000000..ef5629b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/DcdGasOnOff.kt @@ -0,0 +1,5 @@ +package com.laseroptek.raman.data.model.serial + +data class DcdGasOnOff( + val status: Int = 0x54, // 0x54(T), 0x41(A: on), 0x44(D: off) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyControl.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyControl.kt new file mode 100644 index 0000000..f99dfb0 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyControl.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.data.model.serial + + +// type: 0x03 (Control to detect energy) +data class EnergyControl( + val type: Int = 0x03, // Control to detect energy (1Byte) + val status: Int = 0x41, // (1 Byte) + // - 0x41('A': Start), 0x42('B': Stop) : Tx + // - 0x47('G': Good), 0x4E('N': Not good) : Rx + val voltage: Int = 0x000 // 3 bytes +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyControlStop.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyControlStop.kt new file mode 100644 index 0000000..2f1aeae --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyControlStop.kt @@ -0,0 +1,10 @@ +package com.laseroptek.raman.data.model.serial + + +// type: 0x03 (Control to detect energy) +data class EnergyControlStop( + val type: Int = 0x03, // Control to detect energy (1Byte) + val status: Int = 0x41, // (1 Byte) + // - 0x41('A': Start), 0x42('B': Stop) : Tx + // - 0x47('G': Good), 0x4E('N': Not good) : Rx +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyHandpiece.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyHandpiece.kt new file mode 100644 index 0000000..f55c6f9 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyHandpiece.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.data.model.serial + +// type: 0x02 (Check if insert in mount) +// type: 0x02 (Remove the handpiece in mount) +data class EnergyHandpiece( + val type: Int = 0x02, // Handpiece (1 Byte) + val status: Int = 0x41, // (1 Byte) + // - 0x41('A': Check), 0x42('B': Remove) : Tx + // - 0x47('G': Good), 0x4E('N': Not good) : Rx +) + diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyHandpieceRemove.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyHandpieceRemove.kt new file mode 100644 index 0000000..93dd5e1 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyHandpieceRemove.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.data.model.serial + +// type: 0x02 (Check if insert in mount) +// type: 0x02 (Remove the handpiece in mount) +data class EnergyHandpieceRemove( + val type: Int = 0x02, // Handpiece (1 Byte) + val status: Int = 0x42, // (1 Byte) + // - 0x41('A': Check), 0x42('B': Remove) : Tx + // - 0x47('G': Good), 0x4E('N': Not good) : Rx +) + diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyMeasured.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyMeasured.kt new file mode 100644 index 0000000..cdddd8c --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyMeasured.kt @@ -0,0 +1,12 @@ +package com.laseroptek.raman.data.model.serial + +// type: 0x04 (Transmit the measured data) +data class EnergyMeasured( + val type: Int = 0x04, // Transmit the measured data (1 Byte) + val status: Int = 0x45, // (1 Byte) + // - 0x45('E': mEasured), 0x47('G': Good), 0x4E('N': Not good) : Rx + val i: Int = 0x69, + val measured: Int = 0x00, // (8 Byte) : Tx (Internal) + val e: Int = 0x65, + val measured2: Int = 0x00, // (8 Byte) : Tx (Extenal) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyVersion.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyVersion.kt new file mode 100644 index 0000000..9bcaecb --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/EnergyVersion.kt @@ -0,0 +1,8 @@ +package com.laseroptek.raman.data.model.serial + +// type: 0x01 (Version) +data class EnergyVersion( + val type: Int = 0x01, // Version (1 Byte) + val hardware: String = "000A", // n.n.n.m (4 Bytes) : Rx + val software: String = "000A", // n.n.n.m (4 Bytes) : Rx +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/GuideBeam.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/GuideBeam.kt new file mode 100644 index 0000000..744a137 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/GuideBeam.kt @@ -0,0 +1,5 @@ +package com.laseroptek.raman.data.model.serial + +data class GuideBeam( + val value:Int = 0, // ascii 4 bytes (0 ~ 4000) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/HPShotCount.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/HPShotCount.kt new file mode 100644 index 0000000..8c759d7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/HPShotCount.kt @@ -0,0 +1,5 @@ +package com.laseroptek.raman.data.model.serial + +data class HPShotCount( + val count:Int = 0 // ascii (00 ~ 99) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/HandPiece.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/HandPiece.kt new file mode 100644 index 0000000..d04555a --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/HandPiece.kt @@ -0,0 +1,5 @@ +package com.laseroptek.raman.data.model.serial + +data class HandPiece( + val type:Int = 1 // ascii (00 ~ 99) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/LaserStatus.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/LaserStatus.kt new file mode 100644 index 0000000..208c948 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/LaserStatus.kt @@ -0,0 +1,29 @@ +package com.laseroptek.raman.data.model.serial + +import com.laseroptek.raman.const.PulseDurations + +data class LaserStatus( + var laserStatus: Int = 0x53, // 0x53(Standby) or 0x52(Ready) or 0x4C(Laser on) or 0x49(Interval) + var voltage: Int = 0, // ascii 3 byte (PulseWidth() ms, Energy/Fluence() J/cm² : 13 단계 ) -> Votage + var onTime: Float = PulseDurations[0], // xx.x ms (= Pulse PulseWidth = Pulse Duration) : 0.5, 1, 1.5, 4, 5, 10 .. 40 ms : 12 단계) + var frequence: Float = 1f // xx.x Hz (= Repetition? 1, 1.5, 2 hz : 10Hz까지) +) + +val laserStatusMap = mapOf( + 0x53 to "STANDBY", + 0x52 to "READY", + 0x4C to "LASER ON", + 0x49 to "LASER ON", // ""INTERVAL" +) + +fun getLaserStatusString(laserStatus: Int): String { + return laserStatusMap[laserStatus] ?: "STANDBY" +} + +/* cf) +data class Preset( + var pulseWidth: Float = PulseDurations[0], + var fluence: Float = Fluences[0], + var repetition: Float = Repetitions[0], +) +*/ \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/Oven.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/Oven.kt new file mode 100644 index 0000000..24562a2 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/Oven.kt @@ -0,0 +1,7 @@ +package com.laseroptek.raman.data.model.serial + +data class Oven( + val type: Byte = 0x41, + val ktp: Float = 00.0f +) + diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/Packet.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/Packet.kt new file mode 100644 index 0000000..2779b74 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/Packet.kt @@ -0,0 +1,33 @@ +package com.laseroptek.raman.data.model.serial + +data class Packet( + val stx: ByteArray = byteArrayOf(0x21), + val cmd: ByteArray = byteArrayOf(0x00), + val data: ByteArray = byteArrayOf(), + val cs: ByteArray = byteArrayOf(0x0000), + val etx: ByteArray = byteArrayOf(0x0D) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Packet + + if (!stx.contentEquals(other.stx)) return false + if (!cmd.contentEquals(other.cmd)) return false + if (!data.contentEquals(other.data)) return false + if (!cs.contentEquals(other.cs)) return false + if (!etx.contentEquals(other.etx)) return false + + return true + } + + override fun hashCode(): Int { + var result = stx.contentHashCode() + result = 31 * result + cmd.contentHashCode() + result = 31 * result + data.contentHashCode() + result = 31 * result + cs.contentHashCode() + result = 31 * result + etx.contentHashCode() + return result + } +} diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/PurgeBubble.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/PurgeBubble.kt new file mode 100644 index 0000000..2bbdaa2 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/PurgeBubble.kt @@ -0,0 +1,6 @@ +package com.laseroptek.raman.data.model.serial + +// Read - Rx - 16Bytes +data class PurgeBubble( + val value: Int = 0x00 // 0x41 (Accept), 0x42 (Complete), 0x44(Deny), 0x00(Initial) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/QSwitch.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/QSwitch.kt new file mode 100644 index 0000000..257f1da --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/QSwitch.kt @@ -0,0 +1,15 @@ +package com.laseroptek.raman.data.model.serial + +import com.laseroptek.raman.const.DEFAULT_Q_SWITCH_VALUE + +data class QSwitch( + var delayTime: Float = DEFAULT_Q_SWITCH_VALUE, + var intervalTime: Float = DEFAULT_Q_SWITCH_VALUE +) + +val Q_SWITCH_TOGGLE = listOf ( + "+0.5", + "+10.0", + "-10.0", + "-0.5", +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/SprayDcd.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/SprayDcd.kt new file mode 100644 index 0000000..b70df8d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/SprayDcd.kt @@ -0,0 +1,10 @@ +package com.laseroptek.raman.data.model.serial + +import com.laseroptek.raman.const.DEFAULT_SPAY_DCD_DELAY +import com.laseroptek.raman.const.DEFAULT_SPAY_DCD_TIME + +data class SprayDcd( + var status: Int = 0x41, // 1Byte(A(0x41): on, D(0x44): off, P(0x50): pressure) + var sprayTime: Int = DEFAULT_SPAY_DCD_TIME, // Ascii 3Byte (xxx): spray time (010 ~ 100 ms) + var sprayDelay: Int = DEFAULT_SPAY_DCD_DELAY // Ascii 3Byte (xxx): spray delay (010 ~ 100 ms) +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/Temperature.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/Temperature.kt new file mode 100644 index 0000000..e4114eb --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/Temperature.kt @@ -0,0 +1,13 @@ +package com.laseroptek.raman.data.model.serial + +data class Temperature( + val ktp: Float = 50.0f, // Postassium, Titanyl, Phosphate + val chamber1: Float = 80.0f, + val chamber2: Float = 80.0f, + val basePlate: Float = 45.0f, + val water: Float = 80.0f, + val intTemperature: Float = 0.0f, + val intHumidity: Float = 0.0f, + val extTemperature: Float = 0.0f, + val extHumidity: Float = 0.0f, +) diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/TemperatureTimeStamp.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/TemperatureTimeStamp.kt new file mode 100644 index 0000000..e323980 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/TemperatureTimeStamp.kt @@ -0,0 +1,35 @@ +package com.laseroptek.raman.data.model.serial + +/* + val temp = Temperature(ktp = 1.1f) + val temperatureTimeStamp = TemperatureTimeStamp.fromTemperature(temp) + */ +data class TemperatureTimeStamp( + val ktp: Float = 00.0f, + val chamber1: Float = 00.0f, + val chamber2: Float = 00.0f, + val basePlate: Float = 00.0f, + val water: Float = 00.0f, + val intTemperature: Float = 00.0f, + val intHumidity: Float = 00.0f, + val extTemperature: Float = 00.0f, + val extHumidity: Float = 00.0f, + var timestamp: Long = 0L +) { + companion object { + fun fromTemperature(temperature: Temperature, timestamp: Long): TemperatureTimeStamp { + return TemperatureTimeStamp( + ktp = temperature.ktp, + chamber1 = temperature.chamber1, + chamber2 = temperature.chamber2, + basePlate = temperature.basePlate, + water = temperature.water, + intTemperature = temperature.intTemperature, + intHumidity = temperature.intHumidity, + extTemperature = temperature.extTemperature, + extHumidity = temperature.extHumidity, + timestamp = timestamp + ) + } + } +} diff --git a/app/src/main/java/com/laseroptek/raman/data/model/serial/Version.kt b/app/src/main/java/com/laseroptek/raman/data/model/serial/Version.kt new file mode 100644 index 0000000..7f51b86 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/model/serial/Version.kt @@ -0,0 +1,15 @@ +package com.laseroptek.raman.data.model.serial + +import com.laseroptek.raman.BuildConfig + +//import com.laseroptek.raman.const.VERSION_CODE +//import com.laseroptek.raman.const.VERSION_NAME + +data class Version( + var lhHwRev: String = "000A", + var lhSwVer: String = "000A", + var psHwVer: String = "000A", + var psSwVer: String = "000A", + var verCode: String = BuildConfig.VERSION_CODE.toString(), // VERSION_CODE.toString(), // + var verName: String = BuildConfig.VERSION_NAME // VERSION_NAME // +) diff --git a/app/src/main/java/com/laseroptek/raman/data/source/db/DatabaseService.kt b/app/src/main/java/com/laseroptek/raman/data/source/db/DatabaseService.kt new file mode 100644 index 0000000..9145eba --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/db/DatabaseService.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.data.source.db + +import com.laseroptek.raman.data.source.db.model.SerialLog +import kotlinx.coroutines.flow.Flow + +interface DatabaseService { + fun selectSerialLog(maxRecord: Int) : Flow> + + suspend fun insertSerialLog(serialLog: SerialLog) + suspend fun trimLogs(limit: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/source/db/RamanDatabase.kt b/app/src/main/java/com/laseroptek/raman/data/source/db/RamanDatabase.kt new file mode 100644 index 0000000..2d2789d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/db/RamanDatabase.kt @@ -0,0 +1,40 @@ +package com.laseroptek.raman.data.datasource.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.laseroptek.raman.const.DATABASE_VERSION +import com.laseroptek.raman.data.source.db.dao.SerialLogDao +import com.laseroptek.raman.data.source.db.model.SerialLog + +/** + * The [RoomDatabase] we use in this app. + */ +@Database(entities = [SerialLog::class], version = DATABASE_VERSION, exportSchema = false) +abstract class RamanDatabase : RoomDatabase() { + abstract fun serialLogDao(): SerialLogDao + companion object { + const val DATABASE_NAME = "raman.db" + + // Optional: Singleton pattern if not using Hilt (Hilt handles this better) + @Volatile + private var INSTANCE: RamanDatabase? = null + + fun getInstance(context: Context): RamanDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + RamanDatabase::class.java, + DATABASE_NAME + ) + // Add migrations here if DATABASE_VERSION increases + // .addMigrations(MIGRATION_1_2, MIGRATION_2_3) + .fallbackToDestructiveMigration(dropAllTables = true) // Use with caution, deletes data on schema change if no migration + .build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/source/db/RamanDatabaseService.kt b/app/src/main/java/com/laseroptek/raman/data/source/db/RamanDatabaseService.kt new file mode 100644 index 0000000..593fde6 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/db/RamanDatabaseService.kt @@ -0,0 +1,19 @@ +package com.laseroptek.raman.data.source.db + +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.model.SerialLog +import kotlinx.coroutines.flow.Flow + +class RamanDatabaseService(private val ramanDatabase: RamanDatabase) : DatabaseService { + override fun selectSerialLog(maxRecord: Int): Flow> { + return ramanDatabase.serialLogDao().selectSerialLog(maxRecord) + } + + override suspend fun insertSerialLog(serialLog: SerialLog) { + ramanDatabase.serialLogDao().insertSerialLog(serialLog) + } + + override suspend fun trimLogs(limit: Int) { + ramanDatabase.serialLogDao().trimLogs(limit) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/source/db/dao/BaseDao.kt b/app/src/main/java/com/laseroptek/raman/data/source/db/dao/BaseDao.kt new file mode 100644 index 0000000..d445cab --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/db/dao/BaseDao.kt @@ -0,0 +1,27 @@ +package com.laseroptek.raman.data.datasource.db.dao + + +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Update + +/** + * Base DAO. + */ +interface BaseDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: T): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(vararg entity: T) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: Collection) + + @Update(onConflict = OnConflictStrategy.REPLACE) + suspend fun update(entity: T) + + @Delete + suspend fun delete(entity: T): Int +} diff --git a/app/src/main/java/com/laseroptek/raman/data/source/db/dao/SerialLogDao.kt b/app/src/main/java/com/laseroptek/raman/data/source/db/dao/SerialLogDao.kt new file mode 100644 index 0000000..cf49091 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/db/dao/SerialLogDao.kt @@ -0,0 +1,31 @@ +package com.laseroptek.raman.data.source.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction + +import com.laseroptek.raman.data.datasource.db.dao.BaseDao +import com.laseroptek.raman.data.source.db.model.SerialLog +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class SerialLogDao : BaseDao { + @Transaction + @Query("SELECT * FROM serial_log ORDER BY ts DESC LIMIT :maxRecord") + abstract fun selectSerialLog(maxRecord: Int) : Flow> + + @Insert + abstract suspend fun insertSerialLog(serialLog: SerialLog) + + @Query(""" + DELETE FROM serial_log + WHERE id IN ( + SELECT id FROM serial_log + ORDER BY ts ASC + LIMIT (SELECT COUNT(*) FROM serial_log) - :limit + ) + """) + abstract suspend fun trimLogs(limit: Int) + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/source/db/model/SerialLog.kt b/app/src/main/java/com/laseroptek/raman/data/source/db/model/SerialLog.kt new file mode 100644 index 0000000..8125099 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/db/model/SerialLog.kt @@ -0,0 +1,52 @@ +package com.laseroptek.raman.data.source.db.model + +import androidx.compose.runtime.Immutable +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "serial_log", + indices = [ + Index("id", unique = true) + ] +) +@Immutable +data class SerialLog( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, // Optional: auto-generated primary key + val ts: Long = 0, + val cmd: Int = 0, + val tx: Boolean = false, + val w: Boolean = false, + val data: ByteArray = emptyList().toByteArray(), // Room can handle ByteArray + val desc: String = "" +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SerialLog + + if (id != other.id) return false + if (ts != other.ts) return false + if (cmd != other.cmd) return false + if (tx != other.tx) return false + if (w != other.w) return false + if (!data.contentEquals(other.data)) return false + if (desc != other.desc) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + ts.hashCode() + result = 31 * result + cmd + result = 31 * result + tx.hashCode() + result = 31 * result + w.hashCode() + result = 31 * result + data.contentHashCode() + result = 31 * result + desc.hashCode() + return result + } +} diff --git a/app/src/main/java/com/laseroptek/raman/data/source/preference/Preference.kt b/app/src/main/java/com/laseroptek/raman/data/source/preference/Preference.kt new file mode 100644 index 0000000..b5ec628 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/preference/Preference.kt @@ -0,0 +1,91 @@ +package com.laseroptek.raman.data.source.preference + +import com.laseroptek.raman.data.model.LifeTime +import com.laseroptek.raman.data.model.Preset +import com.laseroptek.raman.data.model.Refer2 +import com.laseroptek.raman.data.model.serial.EnergyMeasured +import com.laseroptek.raman.data.model.serial.QSwitch +import com.laseroptek.raman.data.model.serial.SprayDcd +import com.laseroptek.raman.data.model.serial.Temperature +import kotlinx.coroutines.flow.Flow + +interface Preference { + + suspend fun clearAllPreferences() + + suspend fun saveSliderVolumeToPreference(volume: Float) + suspend fun getSliderVolumeFromPreference(): Flow + + suspend fun saveGuideBeamToPreference(guideBeam: Float) + suspend fun getGuideBeamFromPreference(): Flow + + suspend fun saveGuideBeamMinToPreference(guideBeamMin: Int) + suspend fun getGuideBeamMinFromPreference(): Flow + + suspend fun saveGuideBeamMaxToPreference(guideBeamMax: Int) + suspend fun getGuideBeamMaxFromPreference(): Flow + + suspend fun saveHpCountToPreference(hpCount: Int) + suspend fun getHpCountToPreference(): Flow + + suspend fun saveLampCountToPreference(lampCount: Int) + suspend fun getLampCountFromPreference(): Flow + + suspend fun saveDcdCountToPreference(dcdCount: Int) + suspend fun getDcdCountFromPreference(): Flow + + suspend fun saveSprayDcdIndexToPreference(sprayDcdIndex: Int) + suspend fun getSprayDcdIndexFromPreference(): Flow + + suspend fun saveOpTimeHourToPreference(opTimeHour: Long) + suspend fun getOpTimeHourFromPreference(): Flow + + suspend fun saveSprayDcdToPreference(sprayDcd: SprayDcd) + suspend fun getSprayDcdFromPreference(): Flow + + suspend fun saveQSwitchToPreference(qSwitch: QSwitch) + suspend fun getQSwitchFromPreference(): Flow + + suspend fun saveLifeTimeToPreference(lifeTime: LifeTime) + suspend fun getLifeTimeFromPreference(): Flow + + suspend fun saveTemperatureToPreference(temp: Temperature) + suspend fun getTemperatureFromPreference(): Flow + + suspend fun saveTemperatureWriteToPreference(temp: Temperature) + suspend fun getTemperatureWriteFromPreference(): Flow + + suspend fun saveEnergyMeasuredWriteToPreference(temp: EnergyMeasured) + suspend fun getEnergyMeasuredWriteFromPreference(): Flow + + suspend fun saveEnergyRefer2ToPreference(refer2: Refer2) + suspend fun getEnergyRefer2FromPreference(): Flow + + suspend fun getVoltageTableFromPreference( + ): Flow, Int>> + suspend fun saveVoltageTableToPreference( + voltageTable: Map, Int> + ) + + suspend fun savePresetListToPreference( + presetList: List + ) + suspend fun getPresetListFromPreference(): Flow> + + suspend fun saveSprayDcdList( + sprayDcdList: List + ) + suspend fun getSprayDcdList(): Flow> + + ////// + suspend fun saveProductSerialListToPreference(productList: List) + suspend fun getProductSerialListFromPreference(): Flow> + + suspend fun saveLaserHandSerialListToPreference(serialList: List) + suspend fun getLaserHandSerialListFromPreference(): Flow> + + suspend fun savePowerSupplySerialListToPreference(powerSupplySerialList: List) + suspend fun getPowerSupplySerialListFromPreference(): Flow> + ///// + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/source/serial/SerialPort.kt b/app/src/main/java/com/laseroptek/raman/data/source/serial/SerialPort.kt new file mode 100644 index 0000000..ebc424a --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/serial/SerialPort.kt @@ -0,0 +1,128 @@ +package com.laseroptek.raman.data.source.serial + +import android.os.ParcelFileDescriptor +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton // Ensure the wrapper is a singleton if using Hilt +class SerialPort @Inject constructor() { + private var mFD = -1 + private var mDataCallBack: DataCallback? = null + private var preventLibraryLoad = false + + private constructor(preventLoading: Boolean) : this() { + this.preventLibraryLoad = preventLoading + if (preventLoading) { + Timber.d("SerialPort (dummy) created with library loading PREVENTED.") + } + } + + init { + Timber.d("SerialPort instance created. Library loading will be lazy.") + } + + companion object { + @Volatile + private var isLibraryLoaded = false + + /** + * Consolidated Thread-safe lazy loader. + * Using @Synchronized on the companion method ensures the library + * is only loaded once for the entire application process. + */ + @Synchronized + fun ensureLibraryLoaded(): Boolean { + if (!isLibraryLoaded) { + try { + System.loadLibrary("serial_port") + isLibraryLoaded = true + Timber.d("Native library 'serial_port' loaded successfully.") + } catch (e: Exception) { + Timber.e(e, "Failed to load native library 'serial_port'") + } + } + return isLibraryLoaded + } + + fun createDummy(): SerialPort { + return SerialPort(preventLoading = true) + } + } + + fun open(path: String, speed: Int, dataCallBack: DataCallback?) { + // 1. Check if we should load the library + if (!preventLibraryLoad) { + if (!ensureLibraryLoaded()) { + Timber.e("Cannot open port: Native library failed to load.") + dataCallBack?.onData(byteArrayOf(0xE)) + return + } + } else { + Timber.w("Port open called on dummy instance. Skipping native logic.") + return + } + + try { + mDataCallBack = dataCallBack + val file = File(path) + if (!file.exists()) throw Exception("Device path $path does not exist") + + val pFD = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE) + mFD = pFD.detachFd() + pFD.close() + + if (mFD == -1) throw Exception("Failed to get valid FD") + + // Call native open + open(mFD, speed) + Timber.d("Port open success (fd = %d)", mFD) + + } catch (e: Exception) { + Timber.e(e, "Error opening port") + dataCallBack?.onData(byteArrayOf(0xE)) + } + } + + fun write(data: ByteArray) { + if (preventLibraryLoad) { + Timber.w("Write called on dummy. Data: ${data.contentToString()}") + return + } + + val fd = mFD + if (fd != -1 && isLibraryLoaded) { + write(fd, data) + } else { + throw Exception("Port error: FD is -1 or Library not loaded") + } + } + + fun close() { + val fd = mFD + if (fd != -1) { + if (isLibraryLoaded) { + close(fd) + } + mFD = -1 + Timber.d("Port closed.") + } + } + + fun getFD(): Int = mFD + + // --- Native Callbacks & Methods --- + + private fun onNativeData(data: ByteArray) { + if (mFD != -1) mDataCallBack?.onData(data) + } + + private external fun open(fd: Int, speed: Int) + private external fun write(fd: Int, data: ByteArray) + private external fun close(fd: Int) + + interface DataCallback { + fun onData(data: ByteArray) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/data/source/serial/SerialPortInterface.kt b/app/src/main/java/com/laseroptek/raman/data/source/serial/SerialPortInterface.kt new file mode 100644 index 0000000..d884f8b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/data/source/serial/SerialPortInterface.kt @@ -0,0 +1,12 @@ +package com.laseroptek.raman.data.source.serial + +import com.laseroptek.raman.data.model.SerialResult +import kotlinx.coroutines.flow.Flow + +interface SerialPortInterface { + suspend fun open(path: String, speed: Int) : Flow> + fun write(data: ByteArray) : SerialResult + fun close() : SerialResult + fun getFD() : Int + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/di/AppModule.kt b/app/src/main/java/com/laseroptek/raman/di/AppModule.kt new file mode 100644 index 0000000..5c55829 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/di/AppModule.kt @@ -0,0 +1,19 @@ +package com.laseroptek.raman.di + +import android.app.Application +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Provides + @Singleton + fun provideApplicationContext(application: Application): Context { + return application.applicationContext + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/di/DatabaseModule.kt b/app/src/main/java/com/laseroptek/raman/di/DatabaseModule.kt new file mode 100644 index 0000000..f8b7abf --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/di/DatabaseModule.kt @@ -0,0 +1,45 @@ +package com.laseroptek.raman.di + +import android.content.Context +import androidx.room.Room +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.DatabaseService +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.DispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DatabaseModule { + @Provides + @Singleton + fun provideDispatcher(): DispatcherProvider = DefaultDispatcherProvider() + + @Singleton + @Provides + fun provideDatabaseRepository(databaseService: DatabaseService)= DatabaseRepository(databaseService) + + @Provides + @Singleton + fun provideDatabaseService(appDatabase: RamanDatabase): DatabaseService { + return RamanDatabaseService(appDatabase) + } + @Provides + @Singleton + fun provideDatabase( + @ApplicationContext context: Context, + ): RamanDatabase = Room.databaseBuilder(context, RamanDatabase::class.java, RamanDatabase.DATABASE_NAME) + // This is not recommended for normal apps, but the goal of this sample isn't to + // showcase all of Room. + .fallbackToDestructiveMigration(dropAllTables = true) + .build() + + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/di/PreferenceModule.kt b/app/src/main/java/com/laseroptek/raman/di/PreferenceModule.kt new file mode 100644 index 0000000..53e6124 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/di/PreferenceModule.kt @@ -0,0 +1,20 @@ +package com.laseroptek.raman.di + +import android.content.Context +import com.laseroptek.raman.repository.PreferenceRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object PreferenceModule { + + @Singleton + @Provides + fun provideDataStoreRepository(@ApplicationContext context: Context)= PreferenceRepository(context) + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/di/SerialPortModule.kt b/app/src/main/java/com/laseroptek/raman/di/SerialPortModule.kt new file mode 100644 index 0000000..0879ade --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/di/SerialPortModule.kt @@ -0,0 +1,19 @@ +package com.laseroptek.raman.di + +import com.laseroptek.raman.data.source.serial.SerialPort +import com.laseroptek.raman.repository.SerialPortRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SerialPortModule { + + @Singleton + @Provides + fun provideSerialPortRepository(serialPort: SerialPort) = SerialPortRepository(serialPort) + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/navigation/NavigationBar.kt b/app/src/main/java/com/laseroptek/raman/navigation/NavigationBar.kt new file mode 100644 index 0000000..04b185b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/navigation/NavigationBar.kt @@ -0,0 +1,137 @@ +package com.laseroptek.raman.navigation + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.NavigationRailItemDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.ui.common.icon.mainIcon +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.ext.toIntPx +import timber.log.Timber + + +@Composable +fun NavigationSideBar( + items: List, + currentRoute: String?, + innerPadding: PaddingValues, + onItemClick: (NavigationItem) -> Unit = {}, + modifier: Modifier = Modifier +) { + // TestOnly { + Timber.d("top: %d.dp", innerPadding.calculateTopPadding().toIntPx) // top: 15.dp + Timber.d("bottom: %d.dp", innerPadding.calculateBottomPadding().toIntPx) // bottom: 0.dp + // } TestOnly + + NavigationRail( + containerColor = Color.Transparent, // #F7F7F7 + contentColor = Color.Transparent, // #EBEBEB + modifier = modifier + .size(102.px.dp, 316.px.dp) + .padding( + start = 30.px.dp, + //bottom = 60.px.dp + ), + ) { + Column( + modifier = Modifier + .size(72.px.dp, 256.px.dp) + .background(Color(21,21,21), RoundedCornerShape(40.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items.filter { navigationItem -> + navigationItem.show + }.forEach { navigationItem -> + NavigationRailItem( + selected = navigationItem.route == currentRoute, + onClick = { onItemClick(navigationItem) }, + icon = { + Box( + modifier = Modifier + .size(width = 60.px.dp, height = 60.px.dp) + .background(if (navigationItem.route == currentRoute) Color(80,70,35) else Color.Transparent + , RoundedCornerShape(36.px.dp)), + Alignment.Center + ) { + Image( + painter = painterResource(id = mainIcon(navigationItem.route)), + contentDescription = null, + modifier = Modifier.size(24.px.dp), + colorFilter = if (navigationItem.route == currentRoute) ColorFilter.tint(Color(222,173,11)) else ColorFilter.tint(Color.White) + ) + } + }, + modifier = Modifier.size (width = 60.px.dp, height = 60.px.dp) + .align(Alignment.CenterHorizontally) + .clip(RoundedCornerShape(36.px.dp)) + .background(if (navigationItem.route == currentRoute) Color(80,70,35) else Color.Transparent) + , + colors = NavigationRailItemDefaults.colors( + selectedIconColor = Color.White, // Color of the selected icon + unselectedIconColor = Color.Gray, // Color of the unselected icon + selectedTextColor = Color.White, // Color of the selected label + unselectedTextColor = Color.Gray, // Color of the unselected label + indicatorColor = Color.Transparent // Background color of the selected item + ) + ) + } + } + } +} + + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1280dp,height=720dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) + +@Composable +fun PreviewNavigationSideBar() { + Column( + modifier = Modifier + .wrapContentSize() + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val rootNavController = rememberNavController() + val navBackStackEntry by rootNavController.currentBackStackEntryAsState() + val currentRoute by remember(navBackStackEntry) { + derivedStateOf { + navBackStackEntry?.destination?.route + } + } + NavigationSideBar( + items = navigationItemsLists, + currentRoute = currentRoute, + innerPadding = PaddingValues(0.dp), + ) + } +} + diff --git a/app/src/main/java/com/laseroptek/raman/navigation/NavigationItem.kt b/app/src/main/java/com/laseroptek/raman/navigation/NavigationItem.kt new file mode 100644 index 0000000..bf1664a --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/navigation/NavigationItem.kt @@ -0,0 +1,12 @@ +package com.laseroptek.raman.navigation + +import androidx.annotation.StringRes + +data class NavigationItem( + @param:StringRes val title: Int, + val route: String, + val show: Boolean = true, +) + + + diff --git a/app/src/main/java/com/laseroptek/raman/navigation/Routes.kt b/app/src/main/java/com/laseroptek/raman/navigation/Routes.kt new file mode 100644 index 0000000..eb6c079 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/navigation/Routes.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * 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. + */ + +package com.laseroptek.raman.navigation + +import com.laseroptek.raman.R + + +object Graph { + const val NAVIGATION_BAR_SCREEN_GRAPH = "navigationBarScreenGraph" +} + +sealed class Routes(var route: String) { + data object Splash : Routes("splash") + data object Main : Routes("main") + + data object Home : Routes("home") + data object Info : Routes("info") + data object Config : Routes("config") + data object Lock : Routes("lock") + + data object Engineer : Routes("engineer") +} + +val navigationItemsLists: List = listOf( + NavigationItem( + title = R.string.home, + route = Routes.Home.route, + ), + + NavigationItem( + title = R.string.info, + route = Routes.Info.route, + ), + + NavigationItem( + title = R.string.config, + route = Routes.Config.route, + ), + + NavigationItem( + title = R.string.lock, + route = Routes.Lock.route, + ), + + NavigationItem( + title = R.string.engineer, + route = Routes.Engineer.route, + show = false, + ), + +) + +enum class LockNavigation { + Enter, + EnterConfirm, + Lock, + ExitConfirm, +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/navigation/graphs/MainNavGraph.kt b/app/src/main/java/com/laseroptek/raman/navigation/graphs/MainNavGraph.kt new file mode 100644 index 0000000..f2b2240 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/navigation/graphs/MainNavGraph.kt @@ -0,0 +1,91 @@ +package com.laseroptek.raman.navigation.graphs + +import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.navigation.Routes +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.config.ConfigScreen +import com.laseroptek.raman.ui.screens.engineer.EngineerScreen +import com.laseroptek.raman.ui.screens.home.HomeScreen +import com.laseroptek.raman.ui.screens.info.InfoScreen +import com.laseroptek.raman.ui.screens.lock.LockScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.utils.DefaultDispatcherProvider + +@Composable +fun MainNavGraph( + mainNavController: NavHostController = rememberNavController(), + innerPadding: PaddingValues = PaddingValues(), + mainViewModel: MainViewModel = hiltViewModel(), +) { + NavHost( + navController = mainNavController, + startDestination = Routes.Home.route, + ) { + composable(route = Routes.Home.route) { + HomeScreen( + paddingValues = innerPadding, + mainViewModel = mainViewModel, + ) + } + composable(route = Routes.Info.route) { + InfoScreen( + paddingValues = innerPadding, + mainViewModel = mainViewModel, + ) + + } + composable(route = Routes.Config.route) { + ConfigScreen( + mainNavController = mainNavController, + paddingValues = innerPadding, + mainViewModel = mainViewModel, + ) + } + composable(route = Routes.Lock.route) { + LockScreen( + mainNavController = mainNavController + ) + } + composable(route = Routes.Engineer.route) { + EngineerScreen( + mainNavController = mainNavController, + paddingValues = innerPadding, + mainViewModel = mainViewModel, + ) + } + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewMainNavGraph( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + MainNavGraph( + mainViewModel = mainViewModel, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/navigation/graphs/SplashNavGraph.kt b/app/src/main/java/com/laseroptek/raman/navigation/graphs/SplashNavGraph.kt new file mode 100644 index 0000000..56b2559 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/navigation/graphs/SplashNavGraph.kt @@ -0,0 +1,45 @@ +package com.laseroptek.raman.navigation.graphs + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.navigation.Routes +import com.laseroptek.raman.ui.screens.main.MainScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.screens.splash.SplashView + + +@SuppressLint("NewApi") +@Composable +fun SplashNavGraph( + splashNavController: NavHostController = rememberNavController(), + mainNavController: NavHostController = rememberNavController(), + mainViewModel: MainViewModel = hiltViewModel() +) { + NavHost( + navController = splashNavController, + startDestination = Routes.Splash.route, + ) { + composable( + route = Routes.Splash.route + ) { + SplashView( + splashNavController, + mainViewModel = mainViewModel + ) + } + + composable( + route = Routes.Main.route + ) { + MainScreen( + mainNavController, + mainViewModel + ) + } + } +} diff --git a/app/src/main/java/com/laseroptek/raman/repository/DatabaseRepository.kt b/app/src/main/java/com/laseroptek/raman/repository/DatabaseRepository.kt new file mode 100644 index 0000000..cd0d028 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/repository/DatabaseRepository.kt @@ -0,0 +1,30 @@ +package com.laseroptek.raman.repository + +import com.laseroptek.raman.data.source.db.DatabaseService +import com.laseroptek.raman.data.source.db.model.SerialLog +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@ViewModelScoped +class DatabaseRepository @Inject constructor( + private val databaseService: DatabaseService +) { + suspend fun selectSerialLog(maxRecord: Int): Flow> { + return databaseService.selectSerialLog(maxRecord) + } + + suspend fun insertSerialLog(serialLog: SerialLog) { + withContext(Dispatchers.IO) { + databaseService.insertSerialLog(serialLog) + } + } + + suspend fun trimLogs(limit: Int) { + withContext(Dispatchers.IO) { + databaseService.trimLogs(limit) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/repository/PreferenceRepository.kt b/app/src/main/java/com/laseroptek/raman/repository/PreferenceRepository.kt new file mode 100644 index 0000000..f3538a7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/repository/PreferenceRepository.kt @@ -0,0 +1,562 @@ +package com.laseroptek.raman.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.laseroptek.raman.const.PresetList +import com.laseroptek.raman.const.SprayDcdList +import com.laseroptek.raman.const.VoltageTable +import com.laseroptek.raman.data.model.LifeTime +import com.laseroptek.raman.data.model.Preset +import com.laseroptek.raman.data.model.Refer2 +import com.laseroptek.raman.data.model.VoltageEntry +import com.laseroptek.raman.data.model.serial.EnergyMeasured +import com.laseroptek.raman.data.model.serial.QSwitch +import com.laseroptek.raman.data.model.serial.SprayDcd +import com.laseroptek.raman.data.model.serial.Temperature +import com.laseroptek.raman.data.source.preference.Preference +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import timber.log.Timber + +const val DataStore_NAME = "ramanDataStore" + +val Context.datastore : DataStore by preferencesDataStore(name = DataStore_NAME) + +@ViewModelScoped +class PreferenceRepository(private val context: Context) : Preference { + private val gson = Gson() + + companion object{ + // Native Type + val SLIDER_VOLUME = floatPreferencesKey("SLIDER_VOLUME") + val GUIDE_BEAM = floatPreferencesKey("GUIDE_BEAM") + val OP_TIME_HOUR = longPreferencesKey("OP_TIME_HOUR") + val LAMP_COUNT = intPreferencesKey("LAMP_COUNT") + val GUIDE_BEAM_MIN = intPreferencesKey("GUIDE_BEAM_MIN") + val GUIDE_BEAM_MAX = intPreferencesKey("GUIDE_BEAM_MAX") + val DCD_COUNT = intPreferencesKey("DCD_COUNT") + val HP_COUNT = intPreferencesKey("HP_COUNT") + val SPRAY_DCD_INDEX = intPreferencesKey("SPRAY_DCD_INDEX") + + // List of Custom Object Type + val VOLTAGE_TABLE = stringPreferencesKey("VOLTAGE_TABLE") + val PRESET_LIST = stringPreferencesKey("PRESET_LIST") + val SPRAY_DCD_LIST = stringPreferencesKey("SPRAY_DCD_LIST") + + // Custom Object Type + val SPRAY_DCD = stringPreferencesKey("SPRAY_DCD") + val Q_SWITCH = stringPreferencesKey("Q_SWITCH") + val LIFE_TIME = stringPreferencesKey("LIFE_TIME") + val TEMPERATURE = stringPreferencesKey("TEMPERATURE") + val TEMPERATURE_WRITE = stringPreferencesKey("TEMPERATURE_WRITE") + val ENERGY_MEASURED_WRITE = stringPreferencesKey("ENERGY_MEASURED_WRITE") + val ENERGY_REFER_2 = stringPreferencesKey("ENERGY_REFER_2") + + // List of String Object Type + val PRODUCT_SERIAL = stringPreferencesKey("PRODUCT_SERIAL") + val LASER_HAND_SERIAL = stringPreferencesKey("LASER_HAND_SERIAL") + val POWER_SUPPLY_SERIAL = stringPreferencesKey("POWER_SUPPLY_SERIAL") + } + + override suspend fun clearAllPreferences() { + context.datastore.edit { preferences -> + preferences.clear() + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Map of Native Type + + override suspend fun saveSliderVolumeToPreference(volume: Float) { + try { + context.datastore.edit { preferences -> + preferences[SLIDER_VOLUME] = volume + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize SLIDER_VOLUME to JSON.") + } + } + override suspend fun getSliderVolumeFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[SLIDER_VOLUME] ?: 0f + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse SLIDER_VOLUME. Emitting default.") + emit(0f) + } + } + + override suspend fun saveGuideBeamToPreference(guideBeam: Float) { + try { + context.datastore.edit { preferences -> + preferences[GUIDE_BEAM] = guideBeam + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize GUIDE_BEAM to JSON.") + } + } + override suspend fun getGuideBeamFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[GUIDE_BEAM] ?: 0f + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse GUIDE_BEAM. Emitting default.") + emit(0f) + } + } + + override suspend fun saveOpTimeHourToPreference(opTimeHour: Long) { + try { + context.datastore.edit { preferences -> + preferences[OP_TIME_HOUR] = opTimeHour + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize OP_TIME_HOUR to JSON.") + } + } + override suspend fun getOpTimeHourFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[OP_TIME_HOUR] ?: 0L + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse OP_TIME_HOUR. Emitting default.") + emit(0L) + } + } + + override suspend fun saveHpCountToPreference(hpCount: Int) { + try { + context.datastore.edit { preferences -> + preferences[HP_COUNT] = hpCount + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize HP_COUNT to JSON.") + } + } + override suspend fun getHpCountToPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[HP_COUNT] ?: 0 + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse HP_COUNT. Emitting default.") + emit(0) + } + } + + override suspend fun saveLampCountToPreference(lampCount: Int) { + try { + context.datastore.edit { preferences -> + preferences[LAMP_COUNT] = lampCount + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize LAMP_COUNT to JSON.") + } + } + override suspend fun getLampCountFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[LAMP_COUNT] ?: 0 + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse LAMP_COUNT. Emitting default.") + emit(0) + } + } + + override suspend fun saveGuideBeamMinToPreference(guideBeamMin: Int) { + try { + context.datastore.edit { preferences -> + preferences[GUIDE_BEAM_MIN] = guideBeamMin + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize GUIDE_BEAM_MIN to JSON.") + } + } + override suspend fun getGuideBeamMinFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[GUIDE_BEAM_MIN] ?: 0 + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse GUIDE_BEAM_MIN. Emitting default.") + emit(0) + } + } + + override suspend fun saveGuideBeamMaxToPreference(guideBeamMax: Int) { + try { + context.datastore.edit { preferences -> + preferences[GUIDE_BEAM_MAX] = guideBeamMax + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize GUIDE_BEAM_MAX to JSON.") + } + + } + override suspend fun getGuideBeamMaxFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[GUIDE_BEAM_MAX] ?: 0 + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse GUIDE_BEAM_MAX. Emitting default.") + emit(0) + } + } + + override suspend fun saveSprayDcdIndexToPreference(sprayDcdIndex: Int) { + try { + context.datastore.edit { preferences -> + preferences[SPRAY_DCD_INDEX] = sprayDcdIndex + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize SPRAY_SPRAY_DCD_INDEX to JSON.") + } + } + + override suspend fun getSprayDcdIndexFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[SPRAY_DCD_INDEX] ?: 0 + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse SPRAY_SPRAY_DCD_INDEX. Emitting default.") + emit(0) + } + } + + override suspend fun saveDcdCountToPreference(dcdCount: Int) { + try { + context.datastore.edit { preferences -> + preferences[DCD_COUNT] = dcdCount + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize DCD_COUNT to JSON.") + } + } + override suspend fun getDcdCountFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[DCD_COUNT] ?: 0 + }.distinctUntilChanged().catch { e -> + Timber.e(e, "Failed to get or parse DCD_COUNT. Emitting default.") + emit(0) + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Map of Custom Object + + override suspend fun saveSprayDcdToPreference(sprayDcd: SprayDcd) { + try { + val jsonString = gson.toJson(sprayDcd) + context.datastore.edit { preferences -> + preferences[SPRAY_DCD] = jsonString + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize SPRAY_DCD to JSON.") + } + } + override suspend fun getSprayDcdFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[SPRAY_DCD] + }.distinctUntilChanged().map{ jsonString -> + gson.fromJson(jsonString, SprayDcd::class.java) ?: SprayDcd() + }.catch { e -> + Timber.e(e, "Failed to get or parse SPRAY_DCD. Emitting default.") + emit(SprayDcd()) + } + } + + override suspend fun saveQSwitchToPreference(qSwitch: QSwitch) { + try { + val jsonString = gson.toJson(qSwitch) + context.datastore.edit { preferences -> + preferences[Q_SWITCH] = jsonString + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize Q_SWITCH to JSON.") + } + } + override suspend fun getQSwitchFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[Q_SWITCH] + }.distinctUntilChanged().map{ jsonString -> + gson.fromJson(jsonString, QSwitch::class.java) ?: QSwitch() + }.catch { e -> + Timber.e(e, "Failed to get or parse Q_SWITCH. Emitting default.") + emit(QSwitch()) + } + } + + override suspend fun saveLifeTimeToPreference(lifeTime: LifeTime) { + try { + val jsonString = gson.toJson(lifeTime) + context.datastore.edit { preferences -> + preferences[LIFE_TIME] = jsonString + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize LIFE_TIME to JSON.") + } + } + override suspend fun getLifeTimeFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[LIFE_TIME] + }.distinctUntilChanged().map{ jsonString -> + gson.fromJson(jsonString, LifeTime::class.java) ?: LifeTime() + }.catch { e -> + Timber.e(e, "Failed to get or parse LIFE_TIME. Emitting default.") + emit(LifeTime()) + } + } + + override suspend fun saveTemperatureToPreference(temp: Temperature) { + try { + val jsonString = gson.toJson(temp) + context.datastore.edit { preferences -> + preferences[TEMPERATURE] = jsonString + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize TEMPERATURE to JSON.") + } + } + override suspend fun getTemperatureFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[TEMPERATURE] + }.distinctUntilChanged().map{ jsonString -> + gson.fromJson(jsonString, Temperature::class.java) ?: Temperature() + }.catch { e -> + Timber.e(e, "Failed to get or parse TEMPERATURE. Emitting default.") + emit(Temperature()) + } + } + + override suspend fun saveTemperatureWriteToPreference(temp: Temperature) { + try { + val jsonString = gson.toJson(temp) + context.datastore.edit { preferences -> + preferences[TEMPERATURE_WRITE] = jsonString + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize TEMPERATURE_WRITE to JSON.") + } + } + override suspend fun getTemperatureWriteFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[TEMPERATURE_WRITE] + }.distinctUntilChanged().map{ jsonString -> + gson.fromJson(jsonString, Temperature::class.java) ?: Temperature() + }.catch { e -> + Timber.e(e, "Failed to get or parse TEMPERATURE_WRITE. Emitting default.") + emit(Temperature()) + } + } + + override suspend fun saveEnergyMeasuredWriteToPreference(temp: EnergyMeasured) { + try { + val jsonString = gson.toJson(temp) + context.datastore.edit { preferences -> + preferences[ENERGY_MEASURED_WRITE] = jsonString + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize ENERGY_MEASURED_WRITE to JSON.") + } + } + override suspend fun getEnergyMeasuredWriteFromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[ENERGY_MEASURED_WRITE] + }.distinctUntilChanged().map{jsonString -> + gson.fromJson(jsonString, EnergyMeasured::class.java) ?: EnergyMeasured() + }.catch { e -> + Timber.e(e, "Failed to get or parse ENERGY_MEASURED_WRITE. Emitting default.") + emit(EnergyMeasured()) + } + } + + override suspend fun saveEnergyRefer2ToPreference(refer2: Refer2) { + try { + val jsonString = gson.toJson(refer2) + context.datastore.edit { preferences -> + preferences[ENERGY_REFER_2] = jsonString + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize ENERGY_REFER_2 to JSON.") + } + } + override suspend fun getEnergyRefer2FromPreference(): Flow { + return context.datastore.data.map { preferences -> + preferences[ENERGY_REFER_2] + }.distinctUntilChanged().map{ jsonString -> + gson.fromJson(jsonString, Refer2::class.java) ?: Refer2() + }.catch { e -> + Timber.e(e, "Failed to get or parse ENERGY_REFER_2. Emitting default.") + emit(Refer2()) + } + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Map of Pair to Int to List + + override suspend fun saveVoltageTableToPreference(voltageTable: Map, Int>) { + try { + val serializableList = voltageTable.map { (key, value) -> + VoltageEntry(key.first, key.second, value) + } + context.datastore.edit { preferences -> + preferences[VOLTAGE_TABLE] = gson.toJson(serializableList) + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize VOLTAGE_TABLE to JSON.") + } + } + override suspend fun getVoltageTableFromPreference(): Flow, Int>> { + return context.datastore.data.map { preferences -> + preferences[VOLTAGE_TABLE] + }.distinctUntilChanged().map{ jsonString -> + if (!jsonString.isNullOrBlank()) { + val type = object : TypeToken>() {}.type + val list: List = gson.fromJson(jsonString, type) ?: emptyList() + list.associate { Pair(it.pulse, it.energy) to it.voltage } + } else { + VoltageTable + } + }.catch { e -> + Timber.e(e, "Failed to get or parse VOLTAGE_TABLE. Emitting default.") + emit(VoltageTable) + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // List of Custom Object + + override suspend fun savePresetListToPreference(presetList: List) { + try { + context.datastore.edit { preferences -> + preferences[PRESET_LIST] = gson.toJson(presetList) + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize VOLTAGE_TABLE to JSON.") + } + } + + override suspend fun getPresetListFromPreference(): Flow> { + return context.datastore.data.map { preferences -> + preferences[PRESET_LIST] + }.distinctUntilChanged().map { jsonString -> + if (!jsonString.isNullOrBlank()) { + val type = object : TypeToken>() {}.type + gson.fromJson(jsonString, type) ?: PresetList + } else { + PresetList + } + }.catch { e -> + Timber.e(e, "Failed to get or parse PRESET_LIST. Emitting default.") + emit(PresetList) + } + } + + override suspend fun saveSprayDcdList(sprayDcdList: List) { + try { + context.datastore.edit { preferences -> + preferences[SPRAY_DCD_LIST] = gson.toJson(sprayDcdList) + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize SPRAY_DCD_LIST to JSON.") + } + } + + override suspend fun getSprayDcdList(): Flow> { + return context.datastore.data.map { preferences -> + preferences[SPRAY_DCD_LIST] + }.distinctUntilChanged().map { jsonString -> + if (!jsonString.isNullOrBlank()) { + val type = object : TypeToken>() {}.type + gson.fromJson(jsonString, type) ?: SprayDcdList + } else { + SprayDcdList + } + }.catch { e -> + Timber.e(e, "Failed to get or parse SPRAY_DCD_LIST. Emitting default.") + emit(SprayDcdList) + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // List of Native (String) + + override suspend fun saveProductSerialListToPreference(productList: List) { + try { + context.datastore.edit { preferences -> + preferences[PRODUCT_SERIAL] = gson.toJson(productList) + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize PRODUCT_SERIAL to JSON.") + } + } + + override suspend fun getProductSerialListFromPreference(): Flow> { + return context.datastore.data.map { preferences -> + preferences[PRODUCT_SERIAL] + }.distinctUntilChanged().map{ jsonString -> + if (!jsonString.isNullOrBlank()) { + val type = object : TypeToken>() {}.type + gson.fromJson(jsonString, type) ?: listOf("B", "U", "O", "C", "L", "D") + } else { + listOf("B", "U", "O", "C", "L", "D") + } + }.catch { e -> + Timber.e(e, "Failed to get or parse PRODUCT_SERIAL. Emitting default.") + emit(listOf("B", "U", "O", "C", "L", "D")) + } + } + + override suspend fun saveLaserHandSerialListToPreference(serialList: List) { + try { + context.datastore.edit { preferences -> + preferences[LASER_HAND_SERIAL] = gson.toJson(serialList) + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize LASER_HAND_SERIAL to JSON.") + } + } + override suspend fun getLaserHandSerialListFromPreference(): Flow> { + return context.datastore.data.map { preferences -> + preferences[LASER_HAND_SERIAL] + }.distinctUntilChanged().map { jsonString -> + if (!jsonString.isNullOrBlank()) { + val type = object : TypeToken>() {}.type + gson.fromJson(jsonString, type) ?: listOf("B", "U", "O", "C", "L", "D") + } else { + listOf("B", "U", "O", "C", "L", "D") + } + }.catch { e -> + Timber.e(e, "Failed to get or parse LASER_HAND_SERIAL. Emitting default.") + emit(listOf("B", "U", "O", "C", "L", "D")) + } + } + + override suspend fun savePowerSupplySerialListToPreference(powerSupplySerialList: List) { + try { + context.datastore.edit { preferences -> + preferences[POWER_SUPPLY_SERIAL] = gson.toJson(powerSupplySerialList) + } + } catch (e: Exception) { + Timber.e(e, "Failed to serialize POWER_SUPPLY_SERIAL to JSON.") + } + } + override suspend fun getPowerSupplySerialListFromPreference(): Flow> { + return context.datastore.data.map { preferences -> + preferences[POWER_SUPPLY_SERIAL] + }.distinctUntilChanged().map { jsonString -> + if (!jsonString.isNullOrBlank()) { + val type = object : TypeToken>() {}.type + gson.fromJson(jsonString, type) ?: listOf("B", "U", "O", "C", "L", "D") + } else { + listOf("B", "U", "O", "C", "L", "D") + } + }.catch { e -> + Timber.e(e, "Failed to get or parse POWER_SUPPLY_SERIAL. Emitting default.") + emit(listOf("B", "U", "O", "C", "L", "D")) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/repository/SerialPortRepository.kt b/app/src/main/java/com/laseroptek/raman/repository/SerialPortRepository.kt new file mode 100644 index 0000000..28ae0a5 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/repository/SerialPortRepository.kt @@ -0,0 +1,64 @@ +package com.laseroptek.raman.repository + +import com.laseroptek.raman.data.model.SerialResult +import com.laseroptek.raman.data.source.serial.SerialPort +import com.laseroptek.raman.data.source.serial.SerialPortInterface +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import javax.inject.Inject + +@ViewModelScoped +class SerialPortRepository @Inject constructor( + private var serialPort: SerialPort +) : SerialPortInterface { + + companion object { + // Fake constructor 1: Create a SerialPort with a specific (fake) File Descriptor + fun createWithFakeRepository(): SerialPortRepository { + return SerialPortRepository() + } + } + + private constructor() : this( + serialPort = SerialPort.createDummy() + ) + + override suspend fun open(path: String, speed: Int) : Flow> = callbackFlow { + serialPort.open(path, speed, object : SerialPort.DataCallback { + override fun onData(data: ByteArray) { + // 명령단위로 데이터를 끊어서 처리 (한번에 송수신 가능한 크기는 1024 byte (1K)) + if (data.contentEquals(byteArrayOf(0x0E))) { + trySend(SerialResult.Error(Exception("Port error"))) + } else { + trySend(SerialResult.Success(data)) + } + } + }) + awaitClose() + } + + override fun write(data: ByteArray) : SerialResult { + SerialPort.ensureLibraryLoaded() + try { + serialPort.write(data) + return SerialResult.Success("Write success") + } catch (e: Exception) { + return SerialResult.Error(e) + } + } + + override fun close() : SerialResult { + try { + serialPort.close() + return SerialResult.Success("Port close success") + } catch (e: Exception) { + return SerialResult.Error(e) + } + } + + override fun getFD() : Int { + return serialPort.getFD() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt b/app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt new file mode 100644 index 0000000..65c7d8c --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt @@ -0,0 +1,279 @@ +package com.laseroptek.raman.ui + +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.content.res.Resources +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.annotation.RequiresApi +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.const.CMD +import com.laseroptek.raman.const.LASER_STATUS +import com.laseroptek.raman.const.READ_WRITE +import com.laseroptek.raman.ui.screens.main.MainScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.LightColors +import com.laseroptek.raman.ui.theme.MainTheme +import com.laseroptek.raman.utils.SharedPreferenceHelper +import com.laseroptek.raman.utils.SystemSound.setCurrentSystemVolume +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.Locale + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + //////////////////////////////////////////////////////////////////////////////////////////////// + // private val + + /** mainViewModel - MainActivity ViewModel + * - 모든 Screen/View에서 공통으로 사용하는 ViewModel + * - Preference 초기 값 설정. + * - Serial 수신 및 수신 데이터 처리. + * - Serial 송신. + */ + private val mainViewModel: MainViewModel by viewModels() + + + /** selectApkLauncher - ActivityResultLauncher for selecting APK file. + * - apk 설치 파일 선택 및 설치 + */ + //var selectedApkUri: Uri? = null + private val selectApkLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + // This callback is executed when the file chooser is closed. + // Reliably hide the system navigation bar to restore the app's immersive state. + setSystemBarsVisible(false) + + if (result.resultCode == RESULT_OK) { + result.data?.data?.let { uri -> + //selectedApkUri = uri + Timber.d("Selected APK URI: $uri") + } + } else { + Timber.d("No APK file selected") + //selectedApkUri = null + } + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // override functions + + /** attachBaseContext - language setting + * - onCreate() 이전에 호출 됨 + * - Locale 및 Resouce 반영 + */ + override fun attachBaseContext(newBase: Context) { + val selectedLanguage = SharedPreferenceHelper.getString(newBase, "language", "en") ?: "en" + Timber.d("attachBaseContext: language: ${selectedLanguage}") + + val locale = Locale.forLanguageTag(selectedLanguage) + Locale.setDefault(locale) // Set default locale for the app + + val config = Configuration(newBase.resources.configuration) + config.setLocale(locale) + val localizedContext = newBase.createConfigurationContext(config) + super.attachBaseContext(localizedContext) + } + + fun setSystemBarsVisible(visible: Boolean) { + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + if (visible) { + // Makes the bars visible and pushes your app content down. + WindowCompat.setDecorFitsSystemWindows(window, true) + windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) + } else { + // Allows your app content to draw behind the bars (edge-to-edge). + WindowCompat.setDecorFitsSystemWindows(window, false) + windowInsetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + // Hides the bars, making them transparent. + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setSystemBarsVisible(false) + + enableEdgeToEdge() + + // hide soft keyboard (not working) + //window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) + + // Set Full Screen (Immersive Mode) + /* + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.apply { + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + hide(WindowInsetsCompat.Type.statusBars()) + hide(WindowInsetsCompat.Type.navigationBars()) + hide(WindowInsetsCompat.Type.systemBars()) + //hide(WindowInsetsCompat.Type.ime()) // hide soft keyboard (not working) + } + */ + + /* + window.attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + + WindowCompat.setDecorFitsSystemWindows(window, true) + + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) + */ + + + initialize() + + // apk observeUpdateEvents + observeUpdateEvents() + + setContent { + rememberNavController() + val mainNavController = rememberNavController() + + //추후 동적으로 테마 변경 요청시 고려. + //var isDarkTheme by remember { mutableStateOf(false) } // Initial theme state + + MainTheme( + colorScheme = LightColors // DarkColors + ) { + /* + SplashNavGraph( + splashNavController = splashNavController, + mainNavController = mainNavController, + mainViewModel = mainViewModel, + ) + */ + + MainScreen( + mainNavController, + mainViewModel + ) + } + } + } + + override fun onPause() { + super.onPause() + Timber.d("onPause") + + // set laser status (false) + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, byteArrayOf(LASER_STATUS.STAND_BY.toByte())) + } + + override fun onDestroy() { + super.onDestroy() + Timber.d("onDestroy") + + // - not used + //unregisterReceiver(usbReceiver) + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // private functions + + /** observeUpdateEvents - apkUpdateEvent monitoring + * - Engineer Mode에서 Update 시 발생시킨 Event 모니터 + */ + private fun observeUpdateEvents() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + mainViewModel.apkUpdateEvent.collect { + Timber.d("apkUpdateEvent received") + + // Before launching the system file chooser, show the navigation bar. + setSystemBarsVisible(true) + + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/vnd.android.package-archive" // MIME type for APK files + } + selectApkLauncher.launch(intent) + } + } + } + } + + // not working + /* + private fun setAppLocale(languageCode: String) { // e.g., "es", "fr-CA" + val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageCode) + AppCompatDelegate.setApplicationLocales(appLocale) + // No need to manually call recreate() usually, AppCompat handles it. + } + */ + + private fun initialize() { + // Force VM creation on Main thread (instantly) + val vm = mainViewModel + + // Timber.d("density: ${Resources.getSystem().displayMetrics.density}") + + // Use Dispatchers.Default for orchestration to keep IO free for disk + lifecycleScope.launch(Dispatchers.IO) { + try { + // 1. Heavy background work + vm.performFullInitialization() + + withContext(Dispatchers.Main) { + // 2. STAGGERED START: + // Let the Main Thread draw the Loading screen one last time + delay(200) + + // 3. Sync Volume (this can be slow, do it before flipping the UI) + setCurrentSystemVolume( + applicationContext, + AudioManager.STREAM_MUSIC, + vm.sliderVolume.value.toInt() + ) + + // 4. IMPORTANT: Signal initialization + vm.setInitialized(true) + + // 5. POST-RENDER DELAY: + // Wait for the Dashboard to actually draw before starting Serial Loops. + // This prevents serial interrupts from stealing CPU during the first frame. + delay(200) + + vm.txPacketOnce() + + vm.rxPacketLoop() + vm.txPacketLoop() + + Timber.d("System fully operational.") + } + } catch (e: Exception) { + Timber.e(e) + } + } + } + + // End of File + //////////////////////////////////////////////////////////////////////////////////////////////// + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithGradientHIconAndText.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithGradientHIconAndText.kt new file mode 100644 index 0000000..c1dc0e4 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithGradientHIconAndText.kt @@ -0,0 +1,196 @@ +package com.laseroptek.raman.ui.common.button + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun ButtonWithGradientHIconAndText( + modifier: Modifier = Modifier, + painter: Painter = painterResource(id = R.drawable.ic_spray), + title: String = "Purge", + onClick: () -> Unit = {} +) { + Row( + modifier = modifier + .noRippleClickable { + onClick.invoke() + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier.size(14.px.dp, 14.px.dp), + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + colorFilter = ColorFilter.tint(Color.Black) + ) + + Spacer(modifier = Modifier.width(4.px.dp)) + + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black, + ) + } + +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false +) +@Composable +fun PreviewButtonWithGradientHIconAndText() { + Column ( + modifier = Modifier.background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + ButtonWithGradientHIconAndText( + modifier = Modifier + .size(85.px.dp, 32.px.dp) + .clip(RoundedCornerShape(7.px.dp)) + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(43, 20, 63), + 1.0f to Color(123, 74, 152), + ) + ) + ) + .border(1.5.px.dp, color = Color(92, 47, 130), shape = RoundedCornerShape(7.px.dp)) + .dropShadow( + shape = RoundedCornerShape(7.px.dp), + color = Color(243,205,81).copy(alpha = 0.2f), + blur = 3.5.px.dp, + offsetX = 0.px.dp, + offsetY = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(7.px.dp), + color = Color(0,0,0).copy(alpha = 0.1f), + blur = 3.5.px.dp, + offsetX = 1.75.px.dp, + offsetY = 1.75.px.dp, + spread = 0.px.dp + ), + title = "Purge", + painter = painterResource(id = R.drawable.ic_purge), + onClick = { + // Perform action when button is clicked + } + ) + + Spacer(modifier = Modifier.height(16.px.dp)) + + ButtonWithGradientHIconAndText( + modifier = Modifier + .size(76.px.dp, 32.px.dp) + .clip(RoundedCornerShape(7.px.dp)) + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(47, 51, 57), + 1.0f to Color(83, 88, 97), + ) + ) + ) + .border(1.5.px.dp, color = Color(92, 99, 107), shape = RoundedCornerShape(7.px.dp)) + .dropShadow( + shape = RoundedCornerShape(7.px.dp), + color = Color(243,205,81).copy(alpha = 0.2f), + blur = 3.5.px.dp, + offsetX = 0.px.dp, + offsetY = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(7.px.dp), + color = Color(0,0,0).copy(alpha = 0.1f), + blur = 3.5.px.dp, + offsetX = 1.75.px.dp, + offsetY = 1.75.px.dp, + spread = 0.px.dp + ), + title = "Refresh", + painter = painterResource(id = R.drawable.ic_refresh), + onClick = { + // Perform action when button is clicked + } + ) + + Spacer(modifier = Modifier.height(16.px.dp)) + + ButtonWithGradientHIconAndText( + modifier = Modifier + .size(76.px.dp, 32.px.dp) + .clip(RoundedCornerShape(7.px.dp)) + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(82, 63, 2), + 1.0f to Color(204, 156, 0), + ) + ) + ) + .border(1.5.px.dp, color = Color(142, 110, 5), shape = RoundedCornerShape(7.px.dp)) + .dropShadow( + shape = RoundedCornerShape(7.px.dp), + color = Color(243,205,81).copy(alpha = 0.2f), + blur = 3.5.px.dp, + offsetX = 0.px.dp, + offsetY = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(7.px.dp), + color = Color(0,0,0).copy(alpha = 0.1f), + blur = 3.5.px.dp, + offsetX = 1.75.px.dp, + offsetY = 1.75.px.dp, + spread = 0.px.dp + ), + title = "Test", + painter = painterResource(id = R.drawable.ic_spray), + onClick = { + // Perform action when button is clicked + } + ) + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithHIconAndText.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithHIconAndText.kt new file mode 100644 index 0000000..49e1a8d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithHIconAndText.kt @@ -0,0 +1,101 @@ +package com.laseroptek.raman.ui.common.button + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RacingSansOneTypography +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun ButtonWithHIconAndText( + modifier: Modifier = Modifier.size(483.px.dp, 174.px.dp), + text: String, + style: TextStyle = RacingSansOneTypography.titleSmall, + fontSize: TextUnit = 60.px.sp, + textColor: Color = Color.Black, + backgroundColor: Color = Color(255,204,2), + roundedCornerShape: RoundedCornerShape = RoundedCornerShape(topStart = 110.px.dp, topEnd = 24.px.dp, bottomStart = 24.px.dp, bottomEnd = 24.px.dp), + trailingIcon: @Composable() () -> Unit ={}, + leadingIcon: @Composable() () -> Unit ={}, + isDisabled: MutableState = mutableStateOf(false), + onClick: () -> Unit = {} +) { + Surface( + shadowElevation = 4.px.dp, + shape = roundedCornerShape, + color = backgroundColor, + ){ + Row( + modifier = if (! isDisabled.value) modifier.noRippleClickable(onClick = onClick) else modifier, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ){ + leadingIcon() + Spacer(Modifier.width(24.px.dp)) + //Text(text, color = textColor, fontWeight = FontWeight.Bold,) + Text( + text = text, + style = RobotoTypography.bodyMedium.copy( + textAlign = TextAlign.End, + fontWeight = FontWeight.Medium, + ), + fontSize = fontSize, + color = textColor, + ) + trailingIcon() + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false +) + +@Composable +fun PreviewButtonWithHIconAndText() { + Column( + modifier = Modifier.size(483.px.dp, 174.px.dp), + horizontalAlignment = Alignment.CenterHorizontally) { + ButtonWithHIconAndText( + text = "STANDBY", + leadingIcon = { + //Icon(Icons.Filled.Star, contentDescription = null, tint = Color.White) + Image( + painter = painterResource(id = R.drawable.ic_play), + contentDescription = null, + modifier = Modifier.size(49.px.dp) + ) + }, + onClick = { + // Perform action when button is clicked + } + ) + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithVIconAndText.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithVIconAndText.kt new file mode 100644 index 0000000..f32408b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/ButtonWithVIconAndText.kt @@ -0,0 +1,120 @@ +package com.laseroptek.raman.ui.common.button + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +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.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.PretendardTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun ButtonWithVIconAndText( + text: String, + textColor: Color = Color.Black, + backgroundColor: Color = Color(255,204,2), + roundedCornerShape: RoundedCornerShape = RoundedCornerShape(topStart = 110.px.dp, topEnd = 24.px.dp, bottomStart = 24.px.dp, bottomEnd = 24.px.dp), + height: Dp = 174.dp, + width: Dp = 483.dp, + trailingIcon: @Composable() () -> Unit ={}, + leadingIcon: @Composable() () -> Unit ={}, + onClick: () -> Unit = {} +) { + val annotatedString = buildAnnotatedString { + withStyle(SpanStyle(letterSpacing = 0.1.sp)) { // Add letter spacing + append(text) + } + } + + Surface( + color = backgroundColor, + shape = roundedCornerShape + ){ + Box( + modifier = Modifier + .height(height) + .width(width) + //.padding(horizontal = 10.dp) + .noRippleClickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ){ + leadingIcon() + + Spacer(Modifier.height(12.px.dp)) + + //Text(text, color = textColor, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center) + + Text( + text = annotatedString, + color = textColor, + style = PretendardTypography.titleSmall, + fontSize = 24.px.sp, + textAlign = TextAlign.Center, + lineHeight = 24.px.sp + ) + + trailingIcon() + } + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false +) + +@Composable +fun PreviewButtonWithVIconAndText() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + ButtonWithVIconAndText( + text = "TREATMENT SUMMARY", + textColor = Color.White, + backgroundColor = Color.Black, + width = 188.px.dp, + height = 174.px.dp, + roundedCornerShape = RoundedCornerShape(24.px.dp), + leadingIcon = { + //Icon(Icons.Filled.HeartBroken, contentDescription = null, tint = Color.White) + Image( + painter = painterResource(id = R.drawable.ic_heart), + contentDescription = null, + modifier = Modifier.size(44.px.dp) + ) + }, + onClick = { + // Perform action when button is clicked + //onTreatmentSummaryClick() + }, + ) + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/CheckBoxWithText.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/CheckBoxWithText.kt new file mode 100644 index 0000000..00e0aba --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/CheckBoxWithText.kt @@ -0,0 +1,93 @@ +package com.laseroptek.raman.ui.common.button + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun CheckBoxWithText( + label: String = "INTERNAL TEMP.", + state: Boolean = true, + onStateChange: (Boolean) -> Unit = {} +) { + // Checkbox with text on right side + ElevatedCard( + modifier = Modifier.padding(6.px.dp), + shape = RoundedCornerShape(45.px.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.px.dp), + ) { + Row( modifier = Modifier.width(160.px.dp).height(45.px.dp) + .clickable( + role = Role.Checkbox, + onClick = { onStateChange(!state) } + ) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(255,255,255), + Color(209,209,209), + ) + ) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Checkbox( + checked = state, + onCheckedChange = null, + modifier = Modifier.scale(0.7f), + colors = CheckboxDefaults.colors( + checkedColor = Color.White, // Color of the box when checked + uncheckedColor = Color.Gray, // Color of the box when unchecked + checkmarkColor = Color.Black // Color of the checkmark + ) + ) + + Spacer(modifier = Modifier.width(4.px.dp)) + + Text( + text = label, + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/GradientImageButton.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/GradientImageButton.kt new file mode 100644 index 0000000..d714195 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/GradientImageButton.kt @@ -0,0 +1,136 @@ +package com.laseroptek.raman.ui.common.button + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.R +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun GradientImageButton( + externalModifier: Modifier = Modifier, + @DrawableRes id: Int = R.drawable.ic_arrow_down, + buttonWidth: Dp = 24.px.dp, + buttonHeight: Dp = 24.px.dp, + iconWidth: Dp = 16.px.dp, + iconHeight: Dp = 16.px.dp, + onClick: () -> Unit = {} +) { + val localModifier = Modifier + .noRippleClickable { + onClick.invoke() + } + .size(buttonWidth, buttonHeight) + .innerShadow( + color = Color(255,255,255), + blur = 1.1.px.dp, + offsetX = 0.px.dp, + offsetY = 1.1.px.dp, + spread = 0.px.dp, + shape = RoundedCornerShape(6.px.dp), + ) + .dropShadow( + color = Color(0,0,0).copy(alpha = 0.1f), + blur = 3.43.px.dp, + offsetX = 0.px.dp, + offsetY = 0.68.px.dp, + spread = 0.px.dp, + shape = RoundedCornerShape(6.px.dp), + ) + .border( + width = 1.5.px.dp, + brush = Brush.linearGradient( + colors = listOf( + Color(222, 173, 11), + Color(75, 63, 23), + ) + ), + shape = RoundedCornerShape(6.px.dp) + ) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ), + shape = RoundedCornerShape(6.px.dp) + ) + .clip(RoundedCornerShape(6.px.dp)) + Row( + modifier = localModifier.then(externalModifier), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier.size(iconWidth, iconHeight), + painter = painterResource(id = id), + contentDescription = null, + contentScale = ContentScale.Crop, + //colorFilter = ColorFilter.tint(Color.White) + ) + } + +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false +) +@Composable +fun PreviewGradientImageButton() { + Row { + GradientImageButton( + externalModifier = Modifier, + id = R.drawable.ic_arrow_up, + /* + buttonWidth = 32.px.dp, + buttonHeight = 32.px.dp, + iconWidth = 16.px.dp, + iconHeight = 16.px.dp, + */ + onClick = { + // Perform action when button is clicked + } + ) + + Spacer(modifier = Modifier.width(10.px.dp)) + + GradientImageButton( + externalModifier = Modifier, + id = R.drawable.ic_arrow_down, + /* + buttonWidth = 32.px.dp, + buttonHeight = 32.px.dp, + iconWidth = 16.px.dp, + iconHeight = 16.px.dp, + */ + onClick = { + // Perform action when button is clicked + } + ) + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/ImageButton.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/ImageButton.kt new file mode 100644 index 0000000..f9c0465 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/ImageButton.kt @@ -0,0 +1,85 @@ +package com.laseroptek.raman.ui.common.button + +// Removed: import android.graphics.drawable.shapes.Shape +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +// import androidx.compose.foundation.layout.PaddingValues // Not used in this version +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButton // Using Material 3 +// import androidx.compose.material3.Button // Alternative with more customization - Not used in this version +// import androidx.compose.material3.ButtonDefaults // Not used in this version +// import androidx.compose.material3.MaterialTheme // Not used in this version +import androidx.compose.runtime.Composable +// import androidx.compose.ui.Alignment // Not used in this version +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape // Added for parameter type +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.R +import com.laseroptek.raman.utils.ext.px + +@Composable +fun ImageButton( + onClick: () -> Unit, + painter: Painter, + modifier: Modifier = Modifier, + shape: Shape = CircleShape, // Added shape parameter + buttonSize: Dp = 40.px.dp, // Overall size of the button + iconSize: Dp = 40.px.dp, // Size of the icon inside the button + backgroundColor: Color = Color.Transparent, // Optional background + borderColor: Color? = null, // Optional border color + borderWidth: Dp = 0.dp, // Optional border width + contentDescription: String? = null // For accessibility +) { + Box( + modifier = modifier + .size(buttonSize, buttonSize) + .clip(shape) // Use the shape parameter + .background(backgroundColor) + .then( + if (borderColor != null) { + Modifier.border(borderWidth, borderColor, shape) // Use the shape parameter + } else { + Modifier + } + ) + ) { + IconButton( + onClick = onClick, + modifier = Modifier.matchParentSize() // Make IconButton fill the Box + ) { + Image( + painter = painter, + contentDescription = contentDescription, + contentScale = ContentScale.Crop, // Or Crop, depending on your image + modifier = Modifier.size(iconSize) + ) + } + } +} + + +@Preview(showBackground = false) +@Composable +fun ImageButtonPreview() { + //val examplePainter = painterResource(id = R.drawable.ic_purge) // Replace with your actual drawable + + ImageButton( + onClick = { /*TODO*/ }, + painter = painterResource(id = R.drawable.ic_purge_btn) , + // You can now optionally pass a different shape here for testing: + // shape = RoundedCornerShape(8.dp), + //contentDescription = "Android Icon Button", + //borderColor = Color.DarkGray, + //borderWidth = 2.dp + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/ImageButtonWithProgress.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/ImageButtonWithProgress.kt new file mode 100644 index 0000000..2baa737 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/ImageButtonWithProgress.kt @@ -0,0 +1,89 @@ +package com.laseroptek.raman.ui.common.button + +import android.content.res.Configuration +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.R +import com.laseroptek.raman.utils.ext.px + + +@Composable +fun ImageButtonWithProgress( + onClick: () -> Unit, + painter: Painter, + borderWidth: Dp = 0.dp, + borderColor: Color = Color.Transparent, + modifier: Modifier = Modifier, + +) { + val pulsingMinAlpha = 0.5f + val pulsingMaxAlpha = 1.0f + val pulsingAnimationDurationMillis: Int = 1200 + + val infiniteTransition = rememberInfiniteTransition(label = "ImageButtonPulseTransition") + + IconButton( + onClick = onClick, + enabled = true, + modifier = modifier + ) { + Image( + painter = painter, + contentDescription = null, + modifier = modifier, + //.size(width = 90.px.dp, height = 45.px.dp) + //.alpha(pulsingAlpha) + /* + .then( + if (borderWidth > 0.dp) { + Modifier.border(borderWidth, borderColor, RoundedCornerShape(45.px.dp)) + } else { + Modifier + } + ), + */ + contentScale = ContentScale.Crop + ) + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false +) +@Composable +fun ImageButtonWithProgressPreviewCompleted() { + val isErrorState by remember { mutableStateOf(false) } + val currentBorderColor by animateColorAsState( + targetValue = if (isErrorState) Color.Red else MaterialTheme.colorScheme.outline, + label = "BorderColorAnimation" + ) + + ImageButtonWithProgress( + onClick = { }, + painter = painterResource(id = R.drawable.ic_purge_btn), + modifier = Modifier.size(90.px.dp, 45.px.dp) + .clip(RoundedCornerShape(45.px.dp)), + borderWidth = 0.5.dp, + borderColor = currentBorderColor, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/RoundImageButton.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/RoundImageButton.kt new file mode 100644 index 0000000..76b5ca8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/RoundImageButton.kt @@ -0,0 +1,92 @@ +package com.laseroptek.raman.ui.common.button + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.R +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.px + +@Composable +fun RoundImageButton( + onClick: () -> Unit, + painter: Painter, + contentDescription: String?, // Important for accessibility + modifier: Modifier = Modifier, + buttonSize: Dp = 48.dp, // Overall size of the touch target + imageSize: Dp = 24.dp // Size of the image within the button +) { + IconButton( + onClick = onClick, + modifier = modifier + .size(buttonSize) // Set the touch target size for the button + .clip(CircleShape) // This makes the button itself (and its ripple) round + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(255, 255, 255), + 1.0f to Color(209, 209, 209), + ) + ) + ) + .border(1.6.px.dp, color = Color(208, 208, 208), shape = CircleShape) + .dropShadow( + shape = CircleShape, + color = Color(0, 0, 0).copy(alpha = 0.12f), + blur = 10.px.dp, + offsetY = 2.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = CircleShape, + color = Color(240, 240, 240), + blur = 1.6.px.dp, + offsetY = 1.6.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = CircleShape, + color = Color(0, 0, 0).copy(alpha = 0.25f), + blur = 3.2.px.dp, + offsetY = 1.6.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + ) { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.size(imageSize), // Control the size of the image inside + contentScale = ContentScale.Fit // Or ContentScale.Crop, ContentScale.Inside etc. + ) + } +} + +@Preview(showBackground = true) +@Composable +fun RoundImageButtonPreview() { + RoundImageButton( + onClick = { /* Handle click */ }, + painter = painterResource(id = R.drawable.ic_reload), // Replace with your drawable + contentDescription = "Action description", + buttonSize = 50.dp, + imageSize = 24.dp + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/button/RoundTextButton.kt b/app/src/main/java/com/laseroptek/raman/ui/common/button/RoundTextButton.kt new file mode 100644 index 0000000..eeedc97 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/button/RoundTextButton.kt @@ -0,0 +1,101 @@ +package com.laseroptek.raman.ui.common.button + +// Removed: import android.graphics.drawable.shapes.Shape +// import androidx.compose.foundation.layout.PaddingValues // Not used in this version +// import androidx.compose.material3.Button // Alternative with more customization - Not used in this version +// import androidx.compose.material3.ButtonDefaults // Not used in this version +// import androidx.compose.material3.MaterialTheme // Not used in this version +// import androidx.compose.ui.Alignment // Not used in this version +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.px + +@Composable +fun RoundTextButton( + onClick: () -> Unit, + //painter: Painter, + text: String = "H/P", + contentDescription: String?, // Important for accessibility + modifier: Modifier = Modifier, + buttonSize: Dp = 48.dp, // Overall size of the touch target +) { + IconButton( + onClick = onClick, + modifier = modifier + .size(buttonSize) // Set the touch target size for the button + .clip(CircleShape) // This makes the button itself (and its ripple) round + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(255, 255, 255), + 1.0f to Color(209, 209, 209), + ) + ) + ) + .border(1.6.px.dp, color = Color(208, 208, 208), shape = CircleShape) + .dropShadow( + shape = CircleShape, + color = Color(0, 0, 0).copy(alpha = 0.12f), + blur = 10.px.dp, + offsetY = 2.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = CircleShape, + color = Color(240, 240, 240), + blur = 1.6.px.dp, + offsetY = 1.6.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = CircleShape, + color = Color(0, 0, 0).copy(alpha = 0.25f), + blur = 3.2.px.dp, + offsetY = 1.6.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + ) { + Text( + text = text, + style = RobotoTypography.bodySmall.copy( + textAlign = TextAlign.End, + fontSize = 18.px.sp, + fontWeight = FontWeight.Thin, + letterSpacing = 0.2.px.sp, + ), + color = Color.Black + ) + } +} + +@Preview(showBackground = true) +@Composable +fun RoundTextButtonPreview() { + RoundTextButton( + onClick = { /* Handle click */ }, + text = "H/P", + contentDescription = "", + buttonSize = 50.dp, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreen.kt b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreen.kt new file mode 100644 index 0000000..b4a2dcd --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreen.kt @@ -0,0 +1,53 @@ +package com.laseroptek.raman.ui.common.fullscreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput + +@Composable +fun FullScreen( + contentAlignment: Alignment = Alignment.Center, + backgroundColor: Color = Color.Black.copy(alpha = 0.66f), + content: @Composable () -> Unit = {}, +) { + // full screen background + Box( + modifier = Modifier + .fillMaxSize() + // Apply the background color to this Box. + .background(backgroundColor) + // Add a disabled clickable modifier. + // This stabilizes the gesture input system and explicitly consumes clicks, + // preventing them from passing through to elements underneath. + .clickable( + enabled = false, + onClick = {}, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + // Apply the pointerInput to this SAME Box to intercept all touch events + // on its surface, preventing them from reaching UI elements underneath. + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + // Await and consume the event to stop its propagation. + awaitPointerEvent() + } + } + }, + // The contentAlignment will correctly position the content within this Box. + contentAlignment = contentAlignment + ) { + // Place the content directly inside this single, effective Box. + content() + } +} + diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenDialog.kt b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenDialog.kt new file mode 100644 index 0000000..8c6bf3a --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenDialog.kt @@ -0,0 +1,80 @@ +package com.laseroptek.raman.ui.common.fullscreen + +import android.app.Activity +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.DialogWindowProvider +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun FullScreenDialog( + //showPopup: MutableState = mutableStateOf(false), + contentAlignment: Alignment = Alignment.Center, + backgroundColor: Color = Color.Black.copy(alpha = 0.66f), + onDismissRequest: () -> Unit = {}, + content: @Composable () -> Unit = {}, +) { + val context = LocalContext.current + (context as? Activity)?.window // Get the Window from the Activity + ?: throw IllegalStateException("FullscreenImmersiveDialog must be used within an Activity") + + // full screen background + Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false // Make dialog full width + ) + ) { + val window = (LocalView.current.parent as? DialogWindowProvider)?.window + val view = LocalView.current + val windowInsetsController = window?.let { WindowCompat.getInsetsController(it, view) } + DisposableEffect(view) { + windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars()) + onDispose { + windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars()) + } + } + + // 팝업 윈도 외부 키 입력 방지를 위해 한번더 전체 화면으로 박스를 만듬. + Box(modifier = Modifier.fillMaxSize(), contentAlignment = contentAlignment) { + content() + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewFullScreenDialog() { + remember { mutableStateOf(true) } + FullScreenDialog { + Text("FULL SCREEN POPUP") + } +} + diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenPopup.kt new file mode 100644 index 0000000..e220539 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenPopup.kt @@ -0,0 +1,176 @@ +package com.laseroptek.raman.ui.common.fullscreen + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.PretendardTypography +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + + +@Composable +fun FullScreenPopup( + //showPopup: MutableState = mutableStateOf(false), + contentAlignment: Alignment = Alignment.Center, + backgroundColor: Color = Color.Black.copy(alpha = 0.66f), + onDismissRequest: () -> Unit = {}, + content: @Composable () -> Unit = {}, +) { + //if (showPopup.value) { + // full screen background + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor) + ) { + + // 팝업 윈도 외부 키 입력 방지를 위해 한번더 전체 화면으로 박스를 만듬. + Popup( + alignment = contentAlignment, + onDismissRequest = { // This is called when a touch occurs outside the Popup + Timber.d("onDismissRequest") + onDismissRequest.invoke() + }, + properties = PopupProperties( + focusable = true, // Allows intercepting back button and outside touches + dismissOnClickOutside = true // Explicitly enable this + ) + ) { + content() + } + } + //} +} + +@Composable +fun FSTitleLayout( + modifier: Modifier = Modifier, + title: String = stringResource(R.string.serial_number), +) { + // Title + Row(modifier = modifier + .border(width = 2.px.dp, color = Color.Gray), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Column(modifier = Modifier.size(60.px.dp, 90.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.ic_left), + contentDescription = null, + modifier = Modifier.size(45.px.dp), + ) + } + + Column(modifier = Modifier + .fillMaxSize() + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = PretendardTypography.titleSmall, + fontWeight = FontWeight.SemiBold, + fontSize = 30.px.sp, + ) + } + } +} + + +@Composable +fun FSBottomLayout( + onClick : (Boolean) -> Unit = {} +) { + // Bottom (Buttons) + Row(modifier = Modifier + .fillMaxWidth() + .height(80.px.dp) + .border(width = 2.px.dp, color = Color.Gray), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + TextButton( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .border(width = 2.px.dp, color = Color.Gray) + .background(color = Color(235, 235, 235)), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = PretendardTypography.titleSmall, + fontWeight = FontWeight.SemiBold, + fontSize = 30.px.sp, + color = Color.Gray + ) + } + + TextButton( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .border(width = 2.px.dp, color = Color.Gray) + .background(color = Color(255, 204, 0)), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.ok), + style = PretendardTypography.titleSmall, + fontWeight = FontWeight.SemiBold, + fontSize = 30.px.sp, + color = Color.Gray + ) + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewFullScreenPopup() { + remember { mutableStateOf(true) } + FullScreenPopup { + Text("FULL SCREEN POPUP") + } +} + diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenView.kt b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenView.kt new file mode 100644 index 0000000..7bf0244 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/fullscreen/FullScreenView.kt @@ -0,0 +1,47 @@ +package com.laseroptek.raman.ui.common.fullscreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput + +@Composable +fun FullScreenView( + showPopup: MutableState = mutableStateOf(false), + contentAlignment: Alignment = Alignment.Center, + backgroundColor: Color = Color.Black.copy(alpha = 0.66f), + content: @Composable () -> Unit = {}, +) { + if (showPopup.value) { + // full screen background + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor) + ) { + // 팝업 윈도 외부 키 입력 방지를 위해 한번더 전체 화면으로 박스를 만듬. + Box( modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + // 터치나 클릭 이벤트가 하위 뷰로 전달 되는 것을 방지 + // 아니면, 아래 뷰도 같이 동작 하는 문제 발생. + awaitPointerEventScope { + while (true) { + awaitPointerEvent() // Consume the event + } + } + }, + contentAlignment = contentAlignment + ) { + content() + } + } + } +} + diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/icon/Pallas3Icons.kt b/app/src/main/java/com/laseroptek/raman/ui/common/icon/Pallas3Icons.kt new file mode 100644 index 0000000..a08cf08 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/icon/Pallas3Icons.kt @@ -0,0 +1,119 @@ +package com.laseroptek.raman.ui.common.icon + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bookmark +import androidx.compose.material.icons.filled.BookmarkBorder +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.ThumbUpOffAlt +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import com.laseroptek.raman.R +import com.laseroptek.raman.navigation.Routes + +@Composable +fun FavoriteButton(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + imageVector = Icons.Filled.ThumbUpOffAlt, + contentDescription = stringResource(R.string.cd_add_to_favorites) + ) + } +} + +@Composable +fun BookmarkButton( + isBookmarked: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val clickLabel = stringResource( + if (isBookmarked) R.string.unbookmark else R.string.bookmark + ) + IconToggleButton( + checked = isBookmarked, + onCheckedChange = { onClick() }, + modifier = modifier.semantics { + // Use a custom click label that accessibility services can communicate to the user. + // We only want to override the label, not the actual action, so for the action we pass null. + this.onClick(label = clickLabel, action = null) + } + ) { + Icon( + imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Filled.BookmarkBorder, + contentDescription = null // handled by click label of parent + ) + } +} + +@Composable +fun ShareButton(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = stringResource(R.string.cd_share) + ) + } +} + +@Composable +fun TextSettingsButton(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + painter = painterResource(R.drawable.ic_text_settings), + contentDescription = stringResource(R.string.cd_text_settings) + ) + } +} + + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewMainIcon() { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier + .wrapContentSize() + .background(Color.White) + ) { + mainIcon(title = Routes.Home.route, selected = true) + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) + +@Composable +fun mainIcon(title: String = "", selected: Boolean = false): Int { + + @DrawableRes var resId: Int = 0 + when (title) { + Routes.Home.route -> resId = R.drawable.ic_home + Routes.Info.route -> resId = R.drawable.ic_info + Routes.Config.route -> resId = R.drawable.ic_setting + Routes.Lock.route -> resId = R.drawable.ic_lock + } + + return resId +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/lock/OTPBox.kt b/app/src/main/java/com/laseroptek/raman/ui/common/lock/OTPBox.kt new file mode 100644 index 0000000..b222c40 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/lock/OTPBox.kt @@ -0,0 +1,181 @@ +package com.laseroptek.raman.ui.common.lock + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.delay +import timber.log.Timber + +@Composable +fun OTPBox( + modifier: Modifier = Modifier, + keyboardType: KeyboardType = KeyboardType.NumberPassword, + pinLength: Int = 6, + pinValue: String = "", // The actual PIN value + onPinValueChanged: (String) -> Unit, + onDone: (() -> Unit)? = null, + obscuringCharacter: Char = '*', + digitSpacing: Dp = 38.px.dp, + textStyle: TextStyle = LocalTextStyle.current.copy(fontSize = 32.px.sp, textAlign = TextAlign.Center), + autoShowKeyboard: Boolean = true, +) { + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + var textFieldValue by remember(pinValue) { + mutableStateOf( + TextFieldValue( + text = pinValue, + selection = TextRange(pinValue.length) // Place cursor at the end + ) + ) + } + + // Used to visually represent the PIN with obscuring characters + val displayValue = obscuringCharacter.toString().repeat(textFieldValue.text.length) + + LaunchedEffect(Unit) { + if (autoShowKeyboard) { + delay(100) + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } else { + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + //focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + if (!autoShowKeyboard) { + focusManager.clearFocus(force = true) // Hide the keyboard + } + + BasicTextField( + value = textFieldValue.copy(text = displayValue), // Display asterisks + onValueChange = { newTextFieldValue -> + //val newText = newTextFieldValue.text.filter { it.isDigit() } // Allow only digits + val newText = newTextFieldValue.text + val currentActualText = textFieldValue.text + val enteredChar = newText.lastOrNull()?.toString() ?: "" + var updatedPin = currentActualText + + Timber.d("newText: ${newText}") + Timber.d("enteredChar: ${enteredChar}") + + if (newText.length > currentActualText.length) { // Character added + if (currentActualText.length < pinLength) { + updatedPin += enteredChar + } else { + // Replace last character if length limit is reached + updatedPin = currentActualText.dropLast(1) + enteredChar + } + } else if (newText.length < currentActualText.length) { // Character deleted (backspace) + updatedPin = currentActualText.dropLast(1) + } + // Ensure pin does not exceed maxLength (should be handled by above logic mostly) + if (updatedPin.length > pinLength) { + updatedPin = updatedPin.take(pinLength) + } + + textFieldValue = TextFieldValue( + text = updatedPin, // Store actual digits internally + selection = TextRange(updatedPin.length) + ) + + Timber.d("updatedPin: ${updatedPin}") + onPinValueChanged(updatedPin) + }, + modifier = modifier.focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, // KeyboardType.NumberPassword, // KeyboardType.Text, // KeyboardType.NumberPassword, // Or KeyboardType.Number + //imeAction = if (onDone != null) ImeAction.Done else ImeAction.Default + imeAction = ImeAction.Done, + //autoCorrect = false + ), + keyboardActions = KeyboardActions( + onDone = { + Timber.d("onDone()") + onDone?.invoke() + // focusManager.clearFocus() // Hide the keyboard + } + ), + textStyle = textStyle.copy(color = Color.Transparent), // Make actual input transparent + decorationBox = { + // Custom decoration to show asterisks and handle spacing + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Timber.d("displayValue: ${displayValue}") + Timber.d("pinLength: ${pinLength}, displayValue.length: ${displayValue.length}") + + displayValue.forEachIndexed { index, char -> + Text( + text = char.toString(), + style = textStyle, + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding(top = 10.px.dp), + textAlign = TextAlign.Center + ) + } + + // Fill remaining spaces if pin is not complete (optional) + repeat(pinLength - displayValue.length) { + Text( + text = "●", // Placeholder for empty slots, or use a different char + style = textStyle, + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding(bottom = 30.px.dp), + textAlign = TextAlign.Center + ) + } + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelDatePicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelDatePicker.kt new file mode 100644 index 0000000..00c099e --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelDatePicker.kt @@ -0,0 +1,46 @@ +package com.laseroptek.raman.ui.common + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.ui.common.core.DefaultWheelDatePicker +import com.laseroptek.raman.ui.common.core.SelectorProperties +import com.laseroptek.raman.ui.common.core.WheelPickerDefaults +import java.time.LocalDate + +@Composable +fun WheelDatePicker( + modifier: Modifier = Modifier, + startDate: LocalDate = LocalDate.now(), + minDate: LocalDate = LocalDate.MIN, + maxDate: LocalDate = LocalDate.MAX, + yearsRange: IntRange? = IntRange(1922, 2122), + size: DpSize = DpSize(256.dp, 128.dp), + rowCount: Int = 3, + textStyle: TextStyle = MaterialTheme.typography.titleMedium, + textColor: Color = LocalContentColor.current, + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onSnappedDate : (snappedDate: LocalDate) -> Unit = {} +) { + DefaultWheelDatePicker( + modifier, + startDate, + minDate, + maxDate, + yearsRange, + size, + rowCount, + textStyle, + textColor, + selectorProperties, + onSnappedDate = { snappedDate -> + onSnappedDate(snappedDate.snappedLocalDate) + snappedDate.snappedIndex + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelDateTimePicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelDateTimePicker.kt new file mode 100644 index 0000000..7ef9180 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelDateTimePicker.kt @@ -0,0 +1,49 @@ +package com.laseroptek.raman.ui.common + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.ui.common.core.DefaultWheelDateTimePicker +import com.laseroptek.raman.ui.common.core.SelectorProperties +import com.laseroptek.raman.ui.common.core.TimeFormat +import com.laseroptek.raman.ui.common.core.WheelPickerDefaults +import java.time.LocalDateTime + +@Composable +fun WheelDateTimePicker( + modifier: Modifier = Modifier, + startDateTime: LocalDateTime = LocalDateTime.now(), + minDateTime: LocalDateTime = LocalDateTime.MIN, + maxDateTime: LocalDateTime = LocalDateTime.MAX, + yearsRange: IntRange? = IntRange(1922, 2122), + timeFormat: TimeFormat = TimeFormat.HOUR_24, + size: DpSize = DpSize(256.dp, 128.dp), + rowCount: Int = 3, + textStyle: TextStyle = MaterialTheme.typography.titleMedium, + textColor: Color = LocalContentColor.current, + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onSnappedDateTime : (snappedDateTime: LocalDateTime) -> Unit = {} +) { + DefaultWheelDateTimePicker( + modifier, + startDateTime, + minDateTime, + maxDateTime, + yearsRange, + timeFormat, + size, + rowCount, + textStyle, + textColor, + selectorProperties, + onSnappedDateTime = { snappedDateTime -> + onSnappedDateTime(snappedDateTime.snappedLocalDateTime) + snappedDateTime.snappedIndex + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelTextPicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelTextPicker.kt new file mode 100644 index 0000000..fa46db7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelTextPicker.kt @@ -0,0 +1,41 @@ +package com.laseroptek.raman.ui.common.core + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +@Composable +fun WheelTextPicker( + modifier: Modifier = Modifier, + startIndex: Int = 0, + size: DpSize = DpSize(128.dp, 128.dp), + texts: List, + rowCount: Int, + style: TextStyle = MaterialTheme.typography.titleMedium, + color: Color = LocalContentColor.current, + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onScrollFinished: (snappedIndex: Int) -> Int? = { null }, +) { + WheelPicker( + modifier = modifier, + startIndex = startIndex, + size = size, + count = texts.size, + rowCount = rowCount, + selectorProperties = selectorProperties, + onScrollFinished = onScrollFinished + ){ index -> + Text( + text = texts[index], + style = style, + color = color, + maxLines = 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelTimePicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelTimePicker.kt new file mode 100644 index 0000000..db2ef8f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/WheelTimePicker.kt @@ -0,0 +1,47 @@ +package com.laseroptek.raman.ui.common + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.ui.common.core.DefaultWheelTimePicker +import com.laseroptek.raman.ui.common.core.SelectorProperties +import com.laseroptek.raman.ui.common.core.TimeFormat +import com.laseroptek.raman.ui.common.core.WheelPickerDefaults +import java.time.LocalTime + +@Composable +fun WheelTimePicker( + modifier: Modifier = Modifier, + startTime: LocalTime = LocalTime.now(), + minTime: LocalTime = LocalTime.MIN, + maxTime: LocalTime = LocalTime.MAX, + timeFormat: TimeFormat = TimeFormat.HOUR_24, + size: DpSize = DpSize(128.dp, 128.dp), + rowCount: Int = 3, + textStyle: TextStyle = MaterialTheme.typography.titleMedium, + textColor: Color = LocalContentColor.current, + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onSnappedTime : (snappedTime: LocalTime) -> Unit = {}, +) { + DefaultWheelTimePicker( + modifier, + startTime, + minTime, + maxTime, + timeFormat, + size, + rowCount, + textStyle, + textColor, + selectorProperties, + onSnappedTime = { snappedTime, _ -> + onSnappedTime(snappedTime.snappedLocalTime) + snappedTime.snappedIndex + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelDatePicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelDatePicker.kt new file mode 100644 index 0000000..7e2bf3a --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelDatePicker.kt @@ -0,0 +1,281 @@ +package com.laseroptek.raman.ui.common.core + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +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.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import java.text.DateFormatSymbols +import java.time.LocalDate + +@Composable +internal fun DefaultWheelDatePicker( + modifier: Modifier = Modifier, + startDate: LocalDate = LocalDate.now(), + minDate: LocalDate = LocalDate.MIN, + maxDate: LocalDate = LocalDate.MAX, + yearsRange: IntRange? = IntRange(1922, 2122), + size: DpSize = DpSize(256.dp, 128.dp), + rowCount: Int = 3, + textStyle: TextStyle = MaterialTheme.typography.titleMedium, + textColor: Color = LocalContentColor.current, + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onSnappedDate : (snappedDate: SnappedDate) -> Int? = { _ -> null } +) { + var snappedDate by remember { mutableStateOf(startDate) } + + var dayOfMonths = calculateDayOfMonths(snappedDate.month.value, snappedDate.year) + + val months = (1..12).map { + Month( + text = if(size.width / 3 < 55.dp){ + DateFormatSymbols().shortMonths[it - 1] + } else DateFormatSymbols().months[it - 1], + value = it, + index = it - 1 + ) + } + + val years = yearsRange?.map { + Year( + text = it.toString(), + value = it, + index = yearsRange.indexOf(it).takeIf {it != -1} ?: 0 + ) + } + + Box(modifier = modifier, contentAlignment = Alignment.Center){ + if (selectorProperties.enabled().value) { + HorizontalDivider( + modifier = Modifier.padding( + bottom = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + HorizontalDivider( + modifier = Modifier.padding( + top = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + } + Row { + //Day of Month + WheelTextPicker( + size = DpSize( + width = if(yearsRange == null) size.width / 2 else size.width / 3, + height = size.height + ), + texts = dayOfMonths.map { it.text }, + rowCount = rowCount, + style = textStyle, + color = textColor, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + startIndex = dayOfMonths.find { it.value== startDate.dayOfMonth }?.index ?: 0, + onScrollFinished = { snappedIndex -> + + val newDayOfMonth = dayOfMonths.find { it.index == snappedIndex }?.value + + newDayOfMonth?.let { + val newDate = snappedDate.withDayOfMonth(newDayOfMonth) + + if(!newDate.isBefore(minDate) && !newDate.isAfter(maxDate)) { + snappedDate = newDate + } + + val newIndex = dayOfMonths.find { it.value == snappedDate.dayOfMonth }?.index + + newIndex?.let { + onSnappedDate( + SnappedDate.DayOfMonth( + localDate = snappedDate, + index = newIndex + ) + )?.let { return@WheelTextPicker it } + } + } + + return@WheelTextPicker dayOfMonths.find { it.value == snappedDate.dayOfMonth }?.index + } + ) + //Month + WheelTextPicker( + size = DpSize( + width = if(yearsRange == null) size.width / 2 else size.width / 3, + height = size.height + ), + texts = months.map { it.text }, + rowCount = rowCount, + style = textStyle, + color = textColor, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + startIndex = months.find { it.value== startDate.monthValue }?.index ?: 0, + onScrollFinished = { snappedIndex -> + + val newMonth = months.find { it.index == snappedIndex }?.value + + newMonth?.let { + + val newDate = snappedDate.withMonth(newMonth) + + if(!newDate.isBefore(minDate) && !newDate.isAfter(maxDate)) { + snappedDate = newDate + } + + dayOfMonths = calculateDayOfMonths(snappedDate.month.value, snappedDate.year) + + val newIndex = months.find { it.value == snappedDate.monthValue }?.index + + newIndex?.let { + onSnappedDate( + SnappedDate.Month( + localDate = snappedDate, + index = newIndex + ) + )?.let { return@WheelTextPicker it } + } + } + + + return@WheelTextPicker months.find { it.value == snappedDate.monthValue }?.index + } + ) + //Year + years?.let { years -> + WheelTextPicker( + size = DpSize( + width = size.width / 3, + height = size.height + ), + texts = years.map { it.text }, + rowCount = rowCount, + style = textStyle, + color = textColor, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + startIndex = years.find { it.value == startDate.year }?.index ?:0, + onScrollFinished = { snappedIndex -> + + val newYear = years.find { it.index == snappedIndex }?.value + + newYear?.let { + + val newDate = snappedDate.withYear(newYear) + + if(!newDate.isBefore(minDate) && !newDate.isAfter(maxDate)) { + snappedDate = newDate + } + + dayOfMonths = calculateDayOfMonths(snappedDate.month.value, snappedDate.year) + + val newIndex = years.find { it.value == snappedDate.year }?.index + + newIndex?.let { + onSnappedDate( + SnappedDate.Year( + localDate = snappedDate, + index = newIndex + ) + )?.let { return@WheelTextPicker it } + + } + } + + return@WheelTextPicker years.find { it.value == snappedDate.year }?.index + } + ) + } + } + } +} + +internal data class DayOfMonth( + val text: String, + val value: Int, + val index: Int +) + +private data class Month( + val text: String, + val value: Int, + val index: Int +) + +private data class Year( + val text: String, + val value: Int, + val index: Int +) + +internal fun calculateDayOfMonths(month: Int, year: Int): List { + + val isLeapYear = LocalDate.of(year, month, 1).isLeapYear + + val month31day = (1..31).map { + DayOfMonth( + text = it.toString(), + value = it, + index = it - 1 + ) + } + val month30day = (1..30).map { + DayOfMonth( + text = it.toString(), + value = it, + index = it - 1 + ) + } + val month29day = (1..29).map { + DayOfMonth( + text = it.toString(), + value = it, + index = it - 1 + ) + } + val month28day = (1..28).map { + DayOfMonth( + text = it.toString(), + value = it, + index = it - 1 + ) + } + + return when(month){ + 1 -> { month31day } + 2 -> { if(isLeapYear) month29day else month28day } + 3 -> { month31day } + 4 -> { month30day } + 5 -> { month31day } + 6 -> { month30day } + 7 -> { month31day } + 8 -> { month31day } + 9 -> { month30day } + 10 -> { month31day } + 11 -> { month30day } + 12 -> { month31day } + else -> { emptyList() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelDateTimePicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelDateTimePicker.kt new file mode 100644 index 0000000..11b3ef3 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelDateTimePicker.kt @@ -0,0 +1,169 @@ +package com.laseroptek.raman.ui.common.core + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +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.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +@Composable +internal fun DefaultWheelDateTimePicker( + modifier: Modifier = Modifier, + startDateTime: LocalDateTime = LocalDateTime.now(), + minDateTime: LocalDateTime = LocalDateTime.MIN, + maxDateTime: LocalDateTime = LocalDateTime.MAX, + yearsRange: IntRange? = IntRange(1922, 2122), + timeFormat: TimeFormat = TimeFormat.HOUR_24, + size: DpSize = DpSize(256.dp, 128.dp), + rowCount: Int = 3, + textStyle: TextStyle = MaterialTheme.typography.titleMedium, + textColor: Color = LocalContentColor.current, + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onSnappedDateTime : (snappedDateTime: SnappedDateTime) -> Int? = { _ -> null } +) { + + var snappedDateTime by remember { mutableStateOf(startDateTime.truncatedTo(ChronoUnit.MINUTES)) } + + val yearTexts = yearsRange?.map { it.toString() } ?: listOf() + + Box(modifier = modifier, contentAlignment = Alignment.Center){ + if (selectorProperties.enabled().value) { + HorizontalDivider( + modifier = Modifier.padding( + bottom = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + HorizontalDivider( + modifier = Modifier.padding( + top = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + } + Row { + //Date + DefaultWheelDatePicker( + startDate = startDateTime.toLocalDate(), + yearsRange = yearsRange, + size = DpSize( + width = if(yearsRange == null ) size.width * 3 / 6 else size.width * 3 / 5 , + height = size.height + ), + rowCount = rowCount, + textStyle = textStyle, + textColor = textColor, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + onSnappedDate = { snappedDate -> + + val newDateTime = when(snappedDate) { + is SnappedDate.DayOfMonth -> { + snappedDateTime.withDayOfMonth(snappedDate.snappedLocalDate.dayOfMonth) + } + is SnappedDate.Month -> { + snappedDateTime.withMonth(snappedDate.snappedLocalDate.monthValue) + } + is SnappedDate.Year -> { + snappedDateTime.withYear(snappedDate.snappedLocalDate.year) + } + } + + if(!newDateTime.isBefore(minDateTime) && !newDateTime.isAfter(maxDateTime)) { + snappedDateTime = newDateTime + } + + return@DefaultWheelDatePicker when(snappedDate) { + is SnappedDate.DayOfMonth -> { + onSnappedDateTime(SnappedDateTime.DayOfMonth(snappedDateTime,snappedDateTime.dayOfMonth - 1)) + snappedDateTime.dayOfMonth - 1 + } + is SnappedDate.Month -> { + onSnappedDateTime(SnappedDateTime.Month(snappedDateTime,snappedDateTime.month.value - 1)) + snappedDateTime.month.value - 1 + } + is SnappedDate.Year -> { + onSnappedDateTime(SnappedDateTime.Year(snappedDateTime, yearTexts.indexOf(snappedDateTime.year.toString()).takeIf {it != -1} ?: 0)) + yearTexts.indexOf(snappedDateTime.year.toString()).takeIf {it != -1} ?: 0 + } + } + } + ) + //Time + DefaultWheelTimePicker( + startTime = startDateTime.toLocalTime(), + timeFormat = timeFormat, + size = DpSize( + width = if(yearsRange == null ) size.width * 3 / 6 else size.width * 2 / 5 , + height = size.height + ), + rowCount = rowCount, + textStyle = textStyle, + textColor = textColor, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + onSnappedTime = { snappedTime, timeFormat -> + + val newDateTime = when(snappedTime) { + is SnappedTime.Hour -> { + snappedDateTime.withHour(snappedTime.snappedLocalTime.hour) + } + is SnappedTime.Minute -> { + snappedDateTime.withMinute(snappedTime.snappedLocalTime.minute) + } + } + + if(!newDateTime.isBefore(minDateTime) && !newDateTime.isAfter(maxDateTime)) { + snappedDateTime = newDateTime + } + + return@DefaultWheelTimePicker when(snappedTime) { + is SnappedTime.Hour -> { + onSnappedDateTime(SnappedDateTime.Hour(snappedDateTime, snappedDateTime.hour)) + if(timeFormat == TimeFormat.HOUR_24) snappedDateTime.hour else + localTimeToAmPmHour(snappedDateTime.toLocalTime()) - 1 + } + is SnappedTime.Minute -> { + onSnappedDateTime(SnappedDateTime.Minute(snappedDateTime, snappedDateTime.minute)) + snappedDateTime.minute + } + } + } + ) + } + } +} + + + + + + + + + + + + diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelTimePicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelTimePicker.kt new file mode 100644 index 0000000..8da2882 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/DefaultWheelTimePicker.kt @@ -0,0 +1,418 @@ +package com.laseroptek.raman.ui.common.core + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor +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.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import java.time.LocalTime +import java.time.temporal.ChronoUnit + +@Composable +internal fun DefaultWheelTimePicker( + modifier: Modifier = Modifier, + startTime: LocalTime = LocalTime.now(), + minTime: LocalTime = LocalTime.MIN, + maxTime: LocalTime = LocalTime.MAX, + timeFormat: TimeFormat = TimeFormat.HOUR_24, + size: DpSize = DpSize(128.dp, 128.dp), + rowCount: Int = 3, + textStyle: TextStyle = MaterialTheme.typography.titleMedium, + textColor: Color = LocalContentColor.current, + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onSnappedTime : (snappedTime: SnappedTime, timeFormat: TimeFormat) -> Int? = { _,_ -> null }, +) { + + var snappedTime by remember { mutableStateOf(startTime.truncatedTo(ChronoUnit.MINUTES)) } + + val hours = (0..23).map { + Hour( + text = it.toString().padStart(2, '0'), + value = it, + index = it + ) + } + val amPmHours = (1..12).map { + AmPmHour( + text = it.toString(), + value = it, + index = it - 1 + ) + } + + val minutes = (0..59).map { + Minute( + text = it.toString().padStart(2, '0'), + value = it, + index = it + ) + } + + val amPms = listOf( + AmPm( + text = "AM", + value = AmPmValue.AM, + index = 0 + ), + AmPm( + text = "PM", + value = AmPmValue.PM, + index = 1 + ) + ) + + var snappedAmPm by remember { + mutableStateOf( + amPms.find { it.value == amPmValueFromTime(startTime) } ?: amPms[0] + ) + } + + Box(modifier = modifier, contentAlignment = Alignment.Center){ + if (selectorProperties.enabled().value) { + HorizontalDivider( + modifier = Modifier.padding( + bottom = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + HorizontalDivider( + modifier = Modifier.padding( + top = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + } + Row { + //Hour + WheelTextPicker( + size = DpSize( + width = size.width / if(timeFormat == TimeFormat.HOUR_24) 2 else 3, + height = size.height + ), + texts = if(timeFormat == TimeFormat.HOUR_24) hours.map { it.text } else amPmHours.map { it.text }, + rowCount = rowCount, + style = textStyle, + color = textColor, + startIndex = if(timeFormat == TimeFormat.HOUR_24) { + hours.find { it.value == startTime.hour }?.index ?: 0 + } else amPmHours.find { it.value == localTimeToAmPmHour(startTime) }?.index ?: 0, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + onScrollFinished = { snappedIndex -> + + val newHour = if(timeFormat == TimeFormat.HOUR_24) { + hours.find { it.index == snappedIndex }?.value + } else { + amPmHourToHour24( + amPmHours.find { it.index == snappedIndex }?.value ?: 0, + snappedTime.minute, + snappedAmPm.value + ) + } + + newHour?.let { + + val newTime = snappedTime.withHour(newHour) + + if(!newTime.isBefore(minTime) && !newTime.isAfter(maxTime)) { + snappedTime = newTime + } + + val newIndex = if(timeFormat == TimeFormat.HOUR_24) { + hours.find { it.value == snappedTime.hour }?.index + } else { + amPmHours.find { it.value == localTimeToAmPmHour(snappedTime)}?.index + } + + newIndex?.let { + onSnappedTime( + SnappedTime.Hour( + localTime = snappedTime, + index = newIndex + ), + timeFormat + )?.let { return@WheelTextPicker it } + } + } + + return@WheelTextPicker if(timeFormat == TimeFormat.HOUR_24) { + hours.find { it.value == snappedTime.hour }?.index + } else { + amPmHours.find { it.value == localTimeToAmPmHour(snappedTime)}?.index + } + } + ) + //Minute + WheelTextPicker( + size = DpSize( + width = size.width / if(timeFormat == TimeFormat.HOUR_24) 2 else 3, + height = size.height + ), + texts = minutes.map { it.text }, + rowCount = rowCount, + style = textStyle, + color = textColor, + startIndex = minutes.find { it.value == startTime.minute }?.index ?: 0, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + onScrollFinished = { snappedIndex -> + + val newMinute = minutes.find { it.index == snappedIndex }?.value + + val newHour = if(timeFormat == TimeFormat.HOUR_24) { + hours.find { it.value == snappedTime.hour }?.value + } else { + amPmHourToHour24( + amPmHours.find { it.value == localTimeToAmPmHour(snappedTime) }?.value ?: 0, + snappedTime.minute, + snappedAmPm.value + ) + } + + newMinute?.let { + newHour?.let { + val newTime = snappedTime.withMinute(newMinute).withHour(newHour) + + if(!newTime.isBefore(minTime) && !newTime.isAfter(maxTime)) { + snappedTime = newTime + } + + val newIndex = minutes.find { it.value == snappedTime.minute }?.index + + newIndex?.let { + onSnappedTime( + SnappedTime.Minute( + localTime = snappedTime, + index = newIndex + ), + timeFormat + )?.let { return@WheelTextPicker it } + } + } + } + + return@WheelTextPicker minutes.find { it.value == snappedTime.minute }?.index + } + ) + //AM_PM + if(timeFormat == TimeFormat.AM_PM) { + WheelTextPicker( + size = DpSize( + width = size.width / 3, + height = size.height + ), + texts = amPms.map { it.text }, + rowCount = rowCount, + style = textStyle, + color = textColor, + startIndex = amPms.find { it.value == amPmValueFromTime(startTime) }?.index ?:0, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = false + ), + onScrollFinished = { snappedIndex -> + + val newAmPm = amPms.find { + if(snappedIndex == 2) { + it.index == 1 + } else { + it.index == snappedIndex + } + } + + newAmPm?.let { + snappedAmPm = newAmPm + } + + val newMinute = minutes.find { it.value == snappedTime.minute }?.value + + val newHour = amPmHourToHour24( + amPmHours.find { it.value == localTimeToAmPmHour(snappedTime) }?.value ?: 0, + snappedTime.minute, + snappedAmPm.value + ) + + newMinute?.let { + val newTime = snappedTime.withMinute(newMinute).withHour(newHour) + + if(!newTime.isBefore(minTime) && !newTime.isAfter(maxTime)) { + snappedTime = newTime + } + + val newIndex = minutes.find { it.value == snappedTime.hour }?.index + + newIndex?.let { + onSnappedTime( + SnappedTime.Hour( + localTime = snappedTime, + index = newIndex + ), + timeFormat + ) + } + } + + return@WheelTextPicker snappedIndex + } + ) + } + } + Box( + modifier = Modifier + .size( + width = if (timeFormat == TimeFormat.HOUR_24) { + size.width + } else size.width * 2 / 3, + height = size.height / 3 + ) + .align( + alignment = if (timeFormat == TimeFormat.HOUR_24) { + Alignment.Center + } else Alignment.CenterStart + ), + contentAlignment = Alignment.Center + ) { + Text( + text = ":", + style = textStyle, + color = textColor + ) + } + } +} + +enum class TimeFormat { + HOUR_24, AM_PM +} + +private data class Hour( + val text: String, + val value: Int, + val index: Int +) +private data class AmPmHour( + val text: String, + val value: Int, + val index: Int +) + +internal fun localTimeToAmPmHour(localTime: LocalTime): Int { + + if( + isBetween( + localTime, + LocalTime.of(0,0), + LocalTime.of(0,59) + ) + ) { + return localTime.hour + 12 + } + + if( + isBetween( + localTime, + LocalTime.of(1,0), + LocalTime.of(11,59) + ) + ) { + return localTime.hour + } + + if( + isBetween( + localTime, + LocalTime.of(12,0), + LocalTime.of(12,59) + ) + ) { + return localTime.hour + } + + if( + isBetween( + localTime, + LocalTime.of(13,0), + LocalTime.of(23,59) + ) + ) { + return localTime.hour - 12 + } + + return localTime.hour +} + +private fun isBetween(localTime: LocalTime, startTime: LocalTime, endTime: LocalTime): Boolean { + return localTime in startTime..endTime +} + +private fun amPmHourToHour24(amPmHour: Int, amPmMinute: Int, amPmValue: AmPmValue): Int { + + return when(amPmValue) { + AmPmValue.AM -> { + if(amPmHour == 12 && amPmMinute <= 59) { + 0 + } else { + amPmHour + } + } + AmPmValue.PM -> { + if(amPmHour == 12 && amPmMinute <= 59) { + amPmHour + } else { + amPmHour + 12 + } + } + } +} + +private data class Minute( + val text: String, + val value: Int, + val index: Int +) + +private data class AmPm( + val text: String, + val value: AmPmValue, + val index: Int? +) + +internal enum class AmPmValue { + AM, PM +} + +private fun amPmValueFromTime(time: LocalTime): AmPmValue { + return if(time.hour > 11) AmPmValue.PM else AmPmValue.AM +} + + + + + + + + + + + diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/SnappedDateTime.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/SnappedDateTime.kt new file mode 100644 index 0000000..908b126 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/SnappedDateTime.kt @@ -0,0 +1,25 @@ +package com.laseroptek.raman.ui.common.core + +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + + +internal sealed class SnappedDateTime(val snappedLocalDateTime: LocalDateTime, val snappedIndex: Int) { + data class DayOfMonth (val localDateTime: LocalDateTime, val index: Int): SnappedDateTime(localDateTime, index) + data class Month (val localDateTime: LocalDateTime, val index: Int): SnappedDateTime(localDateTime, index) + data class Year (val localDateTime: LocalDateTime, val index: Int): SnappedDateTime(localDateTime, index) + data class Hour (val localDateTime: LocalDateTime, val index: Int): SnappedDateTime(localDateTime, index) + data class Minute (val localDateTime: LocalDateTime, val index: Int): SnappedDateTime(localDateTime, index) +} + +internal sealed class SnappedDate(val snappedLocalDate: LocalDate, val snappedIndex: Int) { + data class DayOfMonth (val localDate: LocalDate, val index: Int): SnappedDate(localDate, index) + data class Month(val localDate: LocalDate, val index: Int): SnappedDate(localDate, index) + data class Year (val localDate: LocalDate, val index: Int): SnappedDate(localDate, index) +} + +internal sealed class SnappedTime(val snappedLocalTime: LocalTime, val snappedIndex: Int) { + data class Hour (val localTime: LocalTime, val index: Int): SnappedTime(localTime, index) + data class Minute (val localTime: LocalTime, val index: Int): SnappedTime(localTime, index) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/WheelPicker.kt b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/WheelPicker.kt new file mode 100644 index 0000000..d0b0762 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/common/picker/core/WheelPicker.kt @@ -0,0 +1,224 @@ +package com.laseroptek.raman.ui.common.core + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import dev.chrisbanes.snapper.ExperimentalSnapperApi +import dev.chrisbanes.snapper.SnapperLayoutInfo +import dev.chrisbanes.snapper.rememberLazyListSnapperLayoutInfo +import dev.chrisbanes.snapper.rememberSnapperFlingBehavior +import kotlin.math.absoluteValue + +@OptIn(ExperimentalSnapperApi::class) +@Composable +internal fun WheelPicker( + modifier: Modifier = Modifier, + startIndex: Int = 0, + count: Int, + rowCount: Int, + size: DpSize = DpSize(128.dp, 128.dp), + selectorProperties: SelectorProperties = WheelPickerDefaults.selectorProperties(), + onScrollFinished: (snappedIndex: Int) -> Int? = { null }, + content: @Composable LazyItemScope.(index: Int) -> Unit, +) { + val lazyListState = rememberLazyListState(startIndex) + val snapperLayoutInfo = rememberLazyListSnapperLayoutInfo(lazyListState = lazyListState) + val isScrollInProgress = lazyListState.isScrollInProgress + + LaunchedEffect(isScrollInProgress, count) { + if(!isScrollInProgress) { + onScrollFinished(calculateSnappedItemIndex(snapperLayoutInfo) ?: startIndex)?.let { + lazyListState.scrollToItem(it) + } + } + } + + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + if (selectorProperties.enabled().value) { + HorizontalDivider( + modifier = Modifier.padding( + bottom = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + HorizontalDivider( + modifier = Modifier.padding( + top = (size.height / rowCount), + start = 10.dp, + end = 10.dp + ), + thickness = (selectorProperties.width().value).dp, + color = selectorProperties.color().value + ) + } + LazyColumn( + modifier = Modifier + .height(size.height) + .width(size.width), + state = lazyListState, + contentPadding = PaddingValues(vertical = size.height / rowCount * ((rowCount - 1 )/ 2)), + flingBehavior = rememberSnapperFlingBehavior( + lazyListState = lazyListState + ) + ){ + items(count){ index -> + val rotationX = calculateAnimatedRotationX( + lazyListState = lazyListState, + snapperLayoutInfo = snapperLayoutInfo, + index = index, + rowCount = rowCount + ) + Box( + modifier = Modifier + .height(size.height / rowCount) + .width(size.width) + .alpha( + calculateAnimatedAlpha( + lazyListState = lazyListState, + snapperLayoutInfo = snapperLayoutInfo, + index = index, + rowCount = rowCount + ) + ) + .graphicsLayer { + this.rotationX = rotationX + }, + contentAlignment = Alignment.Center + ) { + content(index) + } + } + } + } +} + +@OptIn(ExperimentalSnapperApi::class) +private fun calculateSnappedItemIndex(snapperLayoutInfo: SnapperLayoutInfo): Int? { + var currentItemIndex = snapperLayoutInfo.currentItem?.index + + if(snapperLayoutInfo.currentItem?.offset != 0) { + if(currentItemIndex != null) { + currentItemIndex ++ + } + } + return currentItemIndex +} + +@OptIn(ExperimentalSnapperApi::class) +@Composable +private fun calculateAnimatedAlpha( + lazyListState: LazyListState, + snapperLayoutInfo: SnapperLayoutInfo, + index: Int, + rowCount: Int +): Float { + + val distanceToIndexSnap = snapperLayoutInfo.distanceToIndexSnap(index).absoluteValue + val layoutInfo = remember { derivedStateOf { lazyListState.layoutInfo } }.value + val viewPortHeight = layoutInfo.viewportSize.height.toFloat() + val singleViewPortHeight = viewPortHeight / rowCount + + return if(distanceToIndexSnap in 0..singleViewPortHeight.toInt()) { + 1.2f - (distanceToIndexSnap / singleViewPortHeight) + } else { + 0.2f + } +} + +@OptIn(ExperimentalSnapperApi::class) +@Composable +private fun calculateAnimatedRotationX( + lazyListState: LazyListState, + snapperLayoutInfo: SnapperLayoutInfo, + index: Int, + rowCount: Int +): Float { + + val distanceToIndexSnap = snapperLayoutInfo.distanceToIndexSnap(index) + val layoutInfo = remember { derivedStateOf { lazyListState.layoutInfo } }.value + val viewPortHeight = layoutInfo.viewportSize.height.toFloat() + val singleViewPortHeight = viewPortHeight / rowCount + val animatedRotationX = -20f * (distanceToIndexSnap / singleViewPortHeight) + + return if (animatedRotationX.isNaN()) { + 0f + } else { + animatedRotationX + } +} + +object WheelPickerDefaults{ + @Composable + fun selectorProperties( + enabled: Boolean = true, + color: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + width: Float = 2.0f, + ): SelectorProperties = DefaultSelectorProperties( + enabled = enabled, + color = color, + width = width + ) +} + +interface SelectorProperties { + @Composable + fun enabled(): State + @Composable + fun color(): State + @Composable + fun width(): State +} + +@Immutable +internal class DefaultSelectorProperties( + private val enabled: Boolean, + private val color: Color, + private val width: Float +) : SelectorProperties { + + @Composable + override fun enabled(): State { + return rememberUpdatedState(enabled) + } + + @Composable + override fun color(): State { + return rememberUpdatedState(color) + } + + + @Composable + override fun width(): State { + return rememberUpdatedState(width) + } +} + + diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/config/ConfigScreen.kt new file mode 100644 index 0000000..d8a06cb --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/config/ConfigScreen.kt @@ -0,0 +1,857 @@ +package com.laseroptek.raman.ui.screens.config + +import android.content.res.Configuration +import android.media.AudioManager +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.R +import com.laseroptek.raman.const.CMD +import com.laseroptek.raman.const.READ_WRITE +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.serial.GuideBeam +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.SystemSound +import com.laseroptek.raman.utils.SystemSound.setCurrentSystemVolume +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.slider.LabeledRangeSlider +import kotlinx.coroutines.launch +import timber.log.Timber +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfigScreen( + mainNavController: NavHostController = rememberNavController(), + paddingValues: PaddingValues = PaddingValues(), + mainViewModel: MainViewModel = hiltViewModel(), + configViewModel: ConfigViewModel = hiltViewModel() +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val pulseType by mainViewModel.pulseType.collectAsState() + val guideBeam by mainViewModel.guideBeam.collectAsState() + val guideBeamMax by mainViewModel.guideBeamMax.collectAsState() + val guideBeamMin by mainViewModel.guideBeamMin.collectAsState() + + val currentSystemVolume = SystemSound.getCurrentSystemVolume(context, AudioManager.STREAM_MUSIC) + val maxSystemVolume = SystemSound.getMaximumVolumeForStream(context, AudioManager.STREAM_MUSIC) //getMaxSystemVolume(context, AudioManager.STREAM_SYSTEM) + + val sliderVolume by mainViewModel.sliderVolume.collectAsState() + val prevSliderVolume by configViewModel.prevSliderVolume.collectAsState() + + LaunchedEffect(Unit) { + Timber.d("LaunchedEffect - ConfigScreen") + + // set slider volume from system volume + val sliderVolume = currentSystemVolume.coerceIn(0, 10) + //mapSystemVolumeToSliderPosition(currentSystemVolume, maxSystemVolume) + Timber.d("sliderVolume is: $sliderVolume") + //mainViewModel.setSliderVolume(sliderVolume.toFloat()) + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + //keyboardController?.hide() + //focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + Box { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding( + start = 130.px.dp, + end = 30.px.dp, + top = 30.px.dp, + bottom = 30.px.dp + ), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // 1. Top - Engineer Mode Popup + // Engineer Mode + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.Top + ) { + TextButton( + modifier = Modifier + .size(156.px.dp, 44.px.dp) + .clip(RoundedCornerShape(100.px.dp)) + .background(Color(35, 35, 33)) + .shadow( + elevation = 1.px.dp, + shape = RoundedCornerShape(100.px.dp), + ambientColor = Color(0, 0, 0).copy(alpha = 0.1f), + spotColor = Color(0, 0, 0).copy(alpha = 0.2f), + clip = true + ), + onClick = { + mainViewModel.showEngineerPasswordPopup = true + } + ) { + Text( + text = stringResource(R.string.engineer_mode), + style = RobotoTypography.bodyMedium.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.px.sp, + ), + textAlign = TextAlign.Center, + color = Color(243,205,81) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + // 2. Middle - Trigger and Pulse Type Setting + Row(modifier = Modifier + .fillMaxWidth() + .height(100.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ){ + val unSelectedBackground = Brush.verticalGradient( + colors = listOf( + Color(230, 230, 230).copy(alpha = 0.98f), + Color(200, 200, 200), + ) + ) + val selectedBackGround = Color(47, 51, 57) + + // Trigger Type + /* + Column( + modifier = Modifier + .size(268.px.dp, 100.px.dp) + ) { + // Trigger Type - Text + Row(modifier = Modifier + .size(268.px.dp, 30.px.dp) + ) { + Text( + text = stringResource(R.string.trigger_type), + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth(), + style = RobotoTypography.bodyMedium.copy( + color = Color.Black, + fontSize = 20.px.sp, + fontWeight = FontWeight.Normal, + ) + ) + } + + Spacer(modifier = Modifier.height(10.px.dp)) + + // Finger S/W and Foot S/W + Row( + modifier = Modifier + .size(268.px.dp, 50.px.dp) + ) { + // Finger S/W + TextButton( + modifier = Modifier + .size(width = 134.px.dp, height = 50.px.dp) + .clip( + RoundedCornerShape( + topStart = 10.px.dp, + bottomStart = 10.px.dp + ) + ) + .background( + Brush.verticalGradient( + colors = listOf( + Color(250, 250, 250).copy(alpha = 0.98f), + Color(218, 218, 218), + ) + ), + shape = RoundedCornerShape( + topStart = 10.px.dp, + bottomStart = 10.px.dp + ) + ), + onClick = { + Timber.d("Finger S/W clicked") + } + ) { + Text( + text = stringResource(R.string.finger_sw), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + style = RobotoTypography.bodyMedium.copy( + color = Color.Black, + fontSize = 16.px.sp, + ) + ) + } + + // Foot S/W + TextButton( + modifier = Modifier + .size(width = 134.px.dp, height = 50.px.dp) + .clip( + RoundedCornerShape( + topEnd = 10.px.dp, + bottomEnd = 10.px.dp + ) + ) + .background( + color = Color(47, 51, 57), + shape = RoundedCornerShape( + topEnd = 10.px.dp, + bottomEnd = 10.px.dp + ) + ), + onClick = { + Timber.d("Foot S/W clicked") + } + ) { + Text( + text = stringResource(R.string.foot_sw), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + style = RobotoTypography.bodyMedium.copy( + color = Color.White, + fontSize = 16.px.sp, + ) + ) + } + } + } + + Spacer(modifier = Modifier.weight(2f)) + */ + + // Pulse Type + Column( + modifier = Modifier + .size(268.px.dp, 100.px.dp) + ) { + // Pulse Type - Text + Row(modifier = Modifier + .size(268.px.dp, 30.px.dp) + ) { + Text( + text = stringResource(R.string.pulse_type), + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth(), + style = RobotoTypography.bodyMedium.copy( + color = Color.Black, + fontSize = 20.px.sp, + fontWeight = FontWeight.Normal + ) + ) + } + + Spacer(modifier = Modifier.height(10.px.dp)) + + // Single and Repeat + Row( + modifier = Modifier + .size(268.px.dp, 50.px.dp) + ) { + // Single + TextButton( + modifier = Modifier + .size(width = 134.px.dp, height = 50.px.dp) + .clip( + RoundedCornerShape( + topStart = 10.px.dp, + bottomStart = 10.px.dp + ) + ) + .background( + if (pulseType == 0) + SolidColor(selectedBackGround) + else unSelectedBackground, + ), + onClick = { + Timber.d("Single clicked") + mainViewModel.setPulseType(0) + } + ) { + Text( + text = stringResource(R.string.single), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + style = RobotoTypography.bodyMedium.copy( + color = if (pulseType ==0) Color.White else Color.Black, + fontSize = 16.px.sp, + ) + ) + } + + // Repeat + TextButton( + modifier = Modifier + .size(width = 134.px.dp, height = 50.px.dp) + .clip( + RoundedCornerShape( + topEnd = 10.px.dp, + bottomEnd = 10.px.dp + ) + ) + .background( + if (pulseType == 1) SolidColor(selectedBackGround) else unSelectedBackground, + shape = RoundedCornerShape( + topEnd = 10.px.dp, + bottomEnd = 10.px.dp + ) + ), + onClick = { + Timber.d("Repeat clicked") + mainViewModel.setPulseType(1) + } + ) { + Text( + text = stringResource(R.string.repeat), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + style = RobotoTypography.bodyMedium.copy( + color = if (pulseType == 1) Color.White else Color.Black, + fontSize = 16.px.sp, + ) + ) + } + } + } + + Spacer(modifier = Modifier.weight(3f)) + } + + Spacer(modifier = Modifier.height(67.px.dp)) + + // 3. Low - Gudide Beam & Volume Settings + Row(modifier = Modifier + .fillMaxWidth() + .height(140.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ){ + Column(modifier = Modifier + .size(470.px.dp, 140.px.dp) + .clip(RoundedCornerShape(24.px.dp)) + ) { + // Guide Beam + Text( + text = stringResource(R.string.guide_beam), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + style = RobotoTypography.bodyMedium.copy( + fontSize = 20.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 24.px.sp + ), + textAlign = TextAlign.Start, + color = Color.Black + ) + + // Guide Beam Slider & Value(Text) + Row( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Guide Beam Slider + //val value = sliderPositionGuideBeam.value.toInt() + val steps = (0..10).step(1).toList() + //var upperBound by rememberSaveable { mutableStateOf(steps[value]) } // steps[steps.size - 1] + //var upperBound = 10 + + LabeledRangeSlider( + selectedUpperBound = guideBeam.toInt(), + steps = steps, + onRangeChanged = { upper -> + // LIVE update for visual feedback of the slider thumb + mainViewModel.setGuideBeam(upper.toFloat()) + Timber.d("GuideBeam onRangeChanged: $upper") + }, + onRangeChangeFinished = { finalUpper -> + //upperBound = upper + //mainViewModel.setGuideBeam(finalUpper.toFloat()) + scope.launch { + mainViewModel.saveGuideBeamToPreference() + } + + /* + val value = when(guideBeam.toInt()) { + 0 -> 0 + 1 -> guideBeamMin + 10 -> guideBeamMax + else -> (guideBeamMin + (guideBeam.toInt() - 1) * ((guideBeamMax - guideBeamMin) / 9)) + } + */ + val value = (guideBeamMin + (guideBeam.toInt() - 1) * ((guideBeamMax - guideBeamMin) / 9)) + + Timber.d("guideBeam: $value, guideBeamMax: $guideBeamMax, guideBeamMin: $guideBeamMin") + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = value)) + Timber.d("Updated selected range ${steps[0]..finalUpper}") + }, + modifier = Modifier + .fillMaxHeight() + .width(260.px.dp) + .padding(start = 16.dp, end = 16.dp) + ) + + Spacer(Modifier.weight(1f)) + + // ValueBox + Column( + modifier = Modifier + .size(93.px.dp, 50.px.dp) + .background( + Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(250, 250, 250), + ) + ), + shape = RoundedCornerShape(12.px.dp) + ) + .border( + width = 1.px.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(12.px.dp) + ) + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Center + ) { + Text( + text = "${guideBeam.toInt()}", + style = RobotoTypography.bodyMedium.copy( + fontSize = 24.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 32.px.sp, + ), + textAlign = TextAlign.Center, + color = Color.Black, + ) + } + + Spacer(Modifier.weight(1f)) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier + .size(520.px.dp, 140.px.dp) + .clip(RoundedCornerShape(24.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + // Volume + Text( + text = stringResource(R.string.volume), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + style = RobotoTypography.bodyMedium.copy( + fontSize = 20.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black + ) + + // Volume Slider & Value(Text) + Row( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + , verticalAlignment = Alignment.CenterVertically + , horizontalArrangement = Arrangement.Center + ) { + val steps = (0..10.toInt()).step(1).toList() + LabeledRangeSlider( + selectedUpperBound = sliderVolume.toInt(), //sliderPositionVolume.value.toInt(), + steps = steps, + onRangeChanged = { upper -> + mainViewModel.setSliderVolume(upper.toFloat()) + scope.launch { + mainViewModel.saveSliderVolumeToPreference() + } + Timber.d("Volume onRangeChanged: $upper") + }, + onRangeChangeFinished = { finalUpper -> + val finalSystemVolume = mapSliderPositionToSystemVolume(finalUpper.toInt(), maxSystemVolume) + setCurrentSystemVolume( + context, + AudioManager.STREAM_MUSIC, //AudioManager.STREAM_SYSTEM, + finalSystemVolume + ) + scope.launch { + mainViewModel.playBeepSound() + } + Timber.d("Updated selected range ${steps[0]..sliderVolume.toInt()}") + + }, + modifier = Modifier + .width(260.px.dp) + .padding(start = 16.dp, end = 16.dp) + ) + + Spacer(Modifier.weight(1f)) + + // ValueBox + Column( + modifier = Modifier + .size(93.px.dp, 50.px.dp) + .background( + Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(250, 250, 250), + ) + ), + shape = RoundedCornerShape(12.px.dp) + ) + .border( + width = 1.px.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(12.px.dp) + ) + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Center + ) { + Text( + text = "${sliderVolume.toInt()}", + style = RobotoTypography.bodyMedium.copy( + fontSize = 24.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 32.px.sp, + ), + textAlign = TextAlign.Center, + color = Color.Black + ) + } + + Spacer(Modifier.weight(1f)) + + // icon + IconButton( + onClick = { + if (sliderVolume > 0f) { + // UnMute -> Mute State + configViewModel.setPrevSliderVolume( sliderVolume ) + mainViewModel.setSliderVolume(0f) + scope.launch { + mainViewModel.saveSliderVolumeToPreference() + } + setCurrentSystemVolume(context, AudioManager.STREAM_MUSIC, 0) // Also set system volume + Timber.d("UnMute> sliderPositionVolume: ${sliderVolume}") + } else if (prevSliderVolume.toInt() == 0) { + // Mute -> UnMute State + configViewModel.setPrevSliderVolume( 1f ) + mainViewModel.setSliderVolume(1f) + scope.launch { + mainViewModel.saveSliderVolumeToPreference() + } + setCurrentSystemVolume(context, AudioManager.STREAM_MUSIC, 1) + Timber.d("Mute> sliderPositionVolume: 1") + } else { + // Mute -> UnMute State + mainViewModel.setSliderVolume( prevSliderVolume ) + scope.launch { + mainViewModel.saveSliderVolumeToPreference() + } + setCurrentSystemVolume(context, AudioManager.STREAM_MUSIC, prevSliderVolume.toInt()) + Timber.d("Mute> sliderPositionVolume: ${sliderVolume}") + } + + scope.launch { + mainViewModel.playBeepSound() + } + + }, + modifier = Modifier + .size(73.px.dp, 42.px.dp) + .clip(CircleShape) + .background( + brush = Brush.linearGradient( + colorStops = if (sliderVolume < 1f) { + arrayOf( + 0.0f to Color(195, 195, 195), + 1.0f to Color(154, 155, 156), + ) + } else { + arrayOf( + 0.0f to Color(47, 51, 57), + 1.0f to Color(83, 88, 97), + ) + } + ) + ) + .border( + 1.px.dp, + brush = Brush.linearGradient( + colorStops = if (sliderVolume < 1f) { + arrayOf( + 0.0f to Color(230, 230, 230), + 1.0f to Color(196, 196, 196), + ) + } else { + arrayOf( + 0.0f to Color(92, 99, 107), + 1.0f to Color(34, 37, 40), + ) + } + ), + shape = CircleShape + ) + .dropShadow( + shape = CircleShape, + color = Color(0, 0, 0).copy(alpha = 0.2f), + blur = 53.05.px.dp, + offsetY = 2.21.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = CircleShape, + color = Color(0, 0, 0).copy(alpha = 0.1f), + blur = 6.63.px.dp, + offsetY = 3.32.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + ) { + Image( + painter = painterResource( + if (sliderVolume < 1f) { + R.drawable.ic_speaker_off + } else { + R.drawable.ic_speaker_on + } + ), + //painter = painterResource(R.drawable.ic_speaker_off), + contentDescription = null, + colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier + .size(20.px.dp), + contentScale = ContentScale.Crop, + ) + } + Spacer(Modifier.weight(1f)) + } + } + Spacer(modifier = Modifier.weight(1f)) + } + + Spacer(modifier = Modifier.weight(1f)) + + // 4. Bottom - Time & Language Settings + // Time Settings & Language + Row(modifier = Modifier + .fillMaxWidth() + .height(100.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ){ + // Time Setting + ElevatedCard( + modifier = Modifier.padding(2.px.dp), + shape = RoundedCornerShape(45.px.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.px.dp), + ) { + Row( + modifier = Modifier + .size(155.px.dp, 50.px.dp) + .noRippleClickable { + mainViewModel.showTimeSettingPopup = true + } + , horizontalArrangement = Arrangement.Center + , verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_clock), + contentDescription = null, + modifier = Modifier.size(15.px.dp), + contentScale = ContentScale.FillBounds, + colorFilter = ColorFilter.tint(Color.Black) + ) + Spacer(modifier = Modifier.width(10.px.dp)) + Text( + text = stringResource(R.string.time_setting), + style = RobotoTypography.bodyMedium.copy( + color = Color.Black, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + ) + ) + } + } + + /* + Spacer(Modifier.width(10.px.dp)) + + // Language Button + ElevatedCard( + modifier = Modifier.padding(2.px.dp), + shape = RoundedCornerShape(45.px.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.px.dp), + ) { + Row( + modifier = Modifier + .size(155.px.dp, 50.px.dp) + .noRippleClickable { + mainViewModel.showLanguageScreen = true + } + , horizontalArrangement = Arrangement.Center + , verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_globe), + contentDescription = null, + modifier = Modifier.size(18.px.dp), + contentScale = ContentScale.FillBounds, + colorFilter = ColorFilter.tint(Color.Black) + ) + Spacer(modifier = Modifier.width(10.px.dp)) + Text( + text = stringResource(R.string.language), + style = RobotoTypography.bodyMedium.copy( + color = Color.Black, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + ) + ) + } + } + */ + + Spacer(modifier = Modifier.weight(1f)) + } + + Spacer(modifier = Modifier.weight(3f)) + } + + ////////////////////////////////////////////////////// + // Popups --> move to MainView + } +} + +private fun mapSystemVolumeToSliderPosition(currentSystemVolume: Int, maxSystemVolume: Int): Int { + val maxSliderPosition = 10.0f + + if (currentSystemVolume < 0) return 0 + if (maxSystemVolume == 0) return 0 // Avoid division by zero + + // 1. Normalize the volume to a 0.0-1.0 scale + val normalized = currentSystemVolume.toFloat() / maxSystemVolume.toFloat() + + // 2. Scale the normalized value to the slider's range (0.0-10.0) + val scaledValue = normalized * maxSliderPosition + + // 3. Round to the nearest integer and return + return scaledValue.roundToInt().coerceIn(0, 10) +} + +private fun mapSliderPositionToSystemVolume(sliderPosition: Int, maxSystemVolume: Int): Int { + val maxSliderPosition = 10.0f + + // 1. Normalize the slider position to a 0.0-1.0 scale + val normalized = sliderPosition.toFloat() / maxSliderPosition + + // 2. Scale the normalized value to the system volume's range (0.0-15.0) + val scaledValue = normalized * maxSystemVolume + + // 3. Round to the nearest integer, ensure it's within bounds, and return + return scaledValue.roundToInt().coerceIn(0, 15) +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewConfigScreen( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ), + configViewModel:ConfigViewModel = ConfigViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + ConfigScreen( + mainViewModel = mainViewModel, + configViewModel = configViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/config/ConfigViewModel.kt new file mode 100644 index 0000000..a4fa4c5 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/config/ConfigViewModel.kt @@ -0,0 +1,51 @@ +package com.laseroptek.raman.ui.screens.config + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.utils.DispatcherProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ConfigViewModel @Inject constructor( + private val preferenceRepository: PreferenceRepository, + private val serialPortRepository: SerialPortRepository, + private val databaseRepository: DatabaseRepository, + private val dispatcherProvider: DispatcherProvider, + private val applicationContext: Context, +) : ViewModel() { + // volumeLevel + /* + private val _volumeLevel = MutableStateFlow(0f) // Keep the MutableStateFlow private + val volumeLevel = _volumeLevel.asStateFlow() // Expose an immutable StateFlow + fun setVolumeLevel(level: Float) { + _volumeLevel.value = level + } + */ + + // sliderVolume + /* + private val _sliderVolume = MutableStateFlow(0f) // Keep the MutableStateFlow private + val sliderVolume = _sliderVolume.asStateFlow() // Expose an immutable StateFlow + fun setSliderVolume(volume: Float) { + _sliderVolume.value = volume + } + */ + + // sliderVolume (prev - for restore to un mute state) + private val _prevSliderVolume = MutableStateFlow(0f) // Keep the MutableStateFlow private + val prevSliderVolume = _prevSliderVolume.asStateFlow() // Expose an immutable StateFlow + fun setPrevSliderVolume(volume: Float) { + _prevSliderVolume.value = volume + } + + init { + Timber.d("ConfigViewModel init") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/config/engineer/EngineerPasswordPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/config/engineer/EngineerPasswordPopup.kt new file mode 100644 index 0000000..ae4e5e0 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/config/engineer/EngineerPasswordPopup.kt @@ -0,0 +1,47 @@ +package com.laseroptek.raman.ui.screens.config.engineer + +import android.annotation.SuppressLint +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.laseroptek.raman.R +import com.laseroptek.raman.const.ENGINEER_PIN_NUNBER +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import timber.log.Timber + +@SuppressLint("UnusedContentLambdaTargetStateParameter") +@Composable +fun EngineerPasswordPopup( + title: String = stringResource(R.string.engineer_mode_password), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + remember { mutableStateOf(ENGINEER_PIN_NUNBER) } + + TextPinLock( + title = title, + backgroundColor = Color.Transparent, + textColor = Color.Black, + onClick = { ok -> + Timber.d("onClick ok: $ok") + onClick.invoke(ok) + } + ) + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewEngineerPasswordPopup() { + remember { mutableStateOf(true) } + EngineerPasswordPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/config/engineer/TextPinLock.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/config/engineer/TextPinLock.kt new file mode 100644 index 0000000..c16d6eb --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/config/engineer/TextPinLock.kt @@ -0,0 +1,265 @@ +package com.laseroptek.raman.ui.screens.config.engineer + +import android.annotation.SuppressLint +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.ENGINEER_PIN_NUNBER +import com.laseroptek.raman.ui.common.lock.OTPBox +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.delay +import timber.log.Timber + + +@SuppressLint("UnusedContentLambdaTargetStateParameter") +@Composable +fun TextPinLock( + modifier: Modifier = Modifier, + title: String = "", + backgroundColor: Color, + textColor: Color, + onClick: (Boolean) -> Unit, +) { + var pin by remember { mutableStateOf("") } + var animate by remember { mutableStateOf(false) } + var autoShowKeyboard by remember { mutableStateOf(true) } + + LaunchedEffect(animate) { + if (animate) { + // After a short delay (enough for the animation to start), + // reset the trigger. + delay(100) + animate = false + } + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + autoShowKeyboard = false + } + } + + Box( + modifier = modifier + .fillMaxSize() //.systemBarsPadding() + .padding(top = 100.px.dp) + , contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .size(448.px.dp, 280.px.dp) + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Center + ) { + // Pin Lock Box + Column( + modifier = Modifier + .size(448.px.dp, 280.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(color = Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Icon + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.px.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + Image(painter = painterResource(id = R.drawable.ic_lock_small), + contentDescription = null, + modifier = Modifier.size(24.px.dp) + ) + } + + // Title + Row(modifier = Modifier + .fillMaxWidth() + .height(50.px.dp) + , verticalAlignment = Alignment.CenterVertically + , horizontalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = RobotoTypography.headlineSmall, + fontSize = 22.px.sp, + letterSpacing = 0.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black + ) + } + + // PinLock + Box( + modifier = Modifier + .size(398.px.dp, 100.px.dp), + contentAlignment = Alignment.Center + ) { + AnimatedContent( + targetState = animate, + transitionSpec = { + slideInHorizontally( + animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh), + initialOffsetX = { fullWidth -> fullWidth } + ).togetherWith( + slideOutHorizontally( + animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh), + targetOffsetX = { 0 } + ) + ) + }, //modifier = Modifier.wrapContentSize(), label = "Pin" + ) { + //var pin by remember { mutableStateOf("") } + OTPBox( + keyboardType = KeyboardType.NumberPassword, + pinLength = 6, + pinValue = pin, + onPinValueChanged = { newPin -> + pin = newPin + Timber.d("onPinValueChanged: pin = ${newPin}") + }, + onDone = { + Timber.d("onDone") + if (pin == ENGINEER_PIN_NUNBER) { + autoShowKeyboard = false + onClick(true) + } else { + pin = "" + animate = true + } + }, + autoShowKeyboard = autoShowKeyboard + ) + } + } + + Spacer(Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.px.dp) + ) { + Spacer(Modifier.weight(1f)) + + // Cancel - Button + TextButton( + modifier = Modifier + .size(width=80.px.dp, height=40.px.dp), + onClick = { + Timber.d("Cancel Clicked: pin : ${pin}") + autoShowKeyboard = false + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.titleMedium, + fontSize = 15.px.sp, + color = Color.Black + ) + } + + // OK - Button + TextButton( + modifier = Modifier + .size(width=68.px.dp, height=40.px.dp), + onClick = { + Timber.d("OK Clicked -> pin : ${pin}") + if (pin == ENGINEER_PIN_NUNBER) { + autoShowKeyboard = false + onClick(true) + } else { + pin = "" + animate = true + } + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.titleMedium, + fontSize = 15.px.sp, + color = Color.Black + ) + } + } + } + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewTextPinLock() { + TextPinLock( + title = "TITLE", + backgroundColor = Color.Transparent, + textColor = Color.Black, + onClick = { ok -> + Timber.d("onClick ok: $ok") + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/config/language/LanguageView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/config/language/LanguageView.kt new file mode 100644 index 0000000..936a961 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/config/language/LanguageView.kt @@ -0,0 +1,273 @@ +package com.laseroptek.raman.ui.screens.config.language + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat.recreate +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.SharedPreferenceHelper +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.getActivity +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LanguageView( + modifier: Modifier = Modifier, + languageViewModel: LanguageViewModel = hiltViewModel(), + //showLanguageScreen: MutableState = mutableStateOf(false), + //mainViewModel: MainViewModel = hiltViewModel(), + title: String = stringResource(R.string.language), + onClick: (Boolean) -> Unit = {}, +) { + val context = LocalContext.current + val activity= context.getActivity() + + var currentLanguage: String = "" + val selectedLanguage by languageViewModel.selectedLanguage.collectAsState() + + val selectedIndex = remember { mutableStateOf( if (selectedLanguage == "en") 0 else 1) } + val languageList = listOf("en", "ko") + + LaunchedEffect(Unit) { + currentLanguage = SharedPreferenceHelper.getString(context, "language", "en") ?: "en" + Timber.d("attachBaseContext: language: ${selectedLanguage}") + + languageViewModel.setSelectedLanguage(selectedLanguage) + } + + FullScreen { + Column( + modifier = modifier + .size(312.px.dp, 272.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineSmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 22.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // English - Radio + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + RadioButton( + selected = selectedIndex.value == 0, + onClick = { + languageViewModel.setSelectedLanguage("en") + selectedIndex.value = 0 + } + ) + Text( + text = stringResource(R.string.english), + style = RobotoTypography.headlineSmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Korean - Radio + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + RadioButton( + selected = selectedIndex.value == 1, + onClick = { + selectedIndex.value = 1 + languageViewModel.setSelectedLanguage("ko") + } + ) + Text( + text = stringResource(R.string.korean), + style = RobotoTypography.headlineSmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + /* + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + //showLanguageScreen = false + onClick.invoke(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + */ + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onClickOK") + Timber.d("currentLanguage: ${currentLanguage}") + Timber.d("selectedLanguage: ${selectedLanguage}") + + if (currentLanguage != selectedLanguage) { + val selectedLanguage = languageList[selectedIndex.value] + Timber.d("language: ${selectedLanguage}") + + //mainViewModel.setSelectedlanguage(selectedLanguage) + + // Save a string + SharedPreferenceHelper.saveString(context, "language", selectedLanguage) + + if (activity != null) { + // apply new language -> attchBaseContext will be called after restart + recreate(activity) + } + } + onClick.invoke(false) + return@TextButton + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + device = "spec:width=1280dp,height=720dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewLanguageView( + languageViewModel: LanguageViewModel = LanguageViewModel( + /* + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + */ + ) +) { + //val showLanguageScreen = remember { mutableStateOf(true) } + LanguageView( + languageViewModel = languageViewModel + //showLanguageScreen = showLanguageScreen, + //mainViewModel = mainViewModel, + ) + + /* Theme, also colorScheme not working on Preview. + MaterialTheme ( + colorScheme = LightColors // DarkColors + ) { + LanguageView( + showLanguageScreen = showLanguageScreen, + mainViewModel = mainViewModel, + ) + */ +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/config/language/LanguageViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/config/language/LanguageViewModel.kt new file mode 100644 index 0000000..fbaeeec --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/config/language/LanguageViewModel.kt @@ -0,0 +1,24 @@ +package com.laseroptek.raman.ui.screens.config.language + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class LanguageViewModel @Inject constructor( +) : ViewModel() { + // volumeLevel + private val _selectedLanguage = MutableStateFlow("en") // Keep the MutableStateFlow private + val selectedLanguage = _selectedLanguage.asStateFlow() // Expose an immutable StateFlow + + fun setSelectedLanguage(language: String) { + _selectedLanguage.value = language + } + + init { + Timber.d("LanguageViewModel init") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/config/time/TimeSettingPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/config/time/TimeSettingPopup.kt new file mode 100644 index 0000000..cdecaa7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/config/time/TimeSettingPopup.kt @@ -0,0 +1,85 @@ +package com.laseroptek.raman.ui.screens.config.time + +import android.content.Intent +import android.content.res.Configuration +import android.provider.Settings +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import timber.log.Timber + +@Composable +fun TimeSettingPopup( + onDismiss: () -> Unit = {} +) { + val context = LocalContext.current + + // This launcher will be triggered when the settings activity is closed. + val settingsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + // When we return from settings, just dismiss this popup. + Timber.d("Returned from settings, dismissing TimeSettingPopup.") + onDismiss() + } + + // This is the key: Launch the settings screen immediately when this popup becomes visible. + LaunchedEffect(Unit) { + try { + val intent = Intent(Settings.ACTION_DATE_SETTINGS).apply { + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) + } + settingsLauncher.launch(intent) + } catch (e: Exception) { + Timber.e(e, "Could not open Date & Time settings") + Toast.makeText( + context, + "Could not open Date & Time settings.", + Toast.LENGTH_LONG + ).show() + // If it fails, dismiss the popup immediately. + onDismiss() + } + } + + // Display a simple, full-screen, semi-transparent background. + // This creates the new, isolated layout context. + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.6f)), + contentAlignment = Alignment.Center + ) { + // You can optionally show a message here, but for a quick transition, + // this can remain empty. The LaunchedEffect handles the logic. + // Text("Redirecting to settings...", color = Color.White) + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) + +@Composable +fun PreviewTimeSettingView() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + TimeSettingPopup(onDismiss = { + Timber.d("onDismiss") + }) + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/CountItemView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/CountItemView.kt new file mode 100644 index 0000000..0a7ba54 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/CountItemView.kt @@ -0,0 +1,132 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +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.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun CountItemView( + modifier: Modifier = Modifier, + count: Int = 0, + onClick: () -> Unit = {}, + title:String = "" +) { + Row(modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1.2f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "%9d".format(count), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Count", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + //.noRippleClickable(onClick = onDeviceOpTimeClick) + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.End + ) { + Image( + modifier = Modifier + .size(18.px.dp, 18.px.dp) + .noRippleClickable(onClick = { onClick.invoke() }), + painter = painterResource(id = R.drawable.ic_edit), + contentDescription = null, + colorFilter = Color.Black.let { ColorFilter.tint(it) } + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewCountItemView() { + Row { + CountItemView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + title = "Lamp \nTotal Count" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/DcdView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/DcdView.kt new file mode 100644 index 0000000..1413888 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/DcdView.kt @@ -0,0 +1,96 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.data.model.DcdType +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun DcdView( + modifier: Modifier = Modifier, + dcdType: DcdType = DcdType(), + onClick: () -> Unit = {}, + title:String = "DCD" +) { + Row(modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Column( + modifier = Modifier + .noRippleClickable(onClick = onClick) + .fillMaxWidth() + .weight(2.4f) + .height(30.px.dp) + .clip(RoundedCornerShape(10.px.dp)) + .background(Color(47, 51, 57)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "CAN Type: %d ml / DCD Life span : %.1f".format( + dcdType.canType, + dcdType.lifeSpan + ), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 11.px.sp, + color = Color.White + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewDcdView() { + Row { + DcdView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + title = "OpTime 1" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EnergyDetectView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EnergyDetectView.kt new file mode 100644 index 0000000..2f5d2b8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EnergyDetectView.kt @@ -0,0 +1,122 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun EnergyDetectView( + modifier: Modifier = Modifier, + value: Float = 0f, + onClick: (Int) -> Unit = {}, + title:String = "ENERGY \nDETECT" +) { + Row(modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // TITLE + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.bodyMedium.copy( + //letterSpacing = 3.px.sp, + lineHeight = 12.px.sp + ), + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + // VALUE + Column( + modifier = Modifier + .noRippleClickable(onClick = { onClick.invoke(1) }) + .fillMaxWidth() + .weight(1.15f) + .height(30.px.dp) + .clip(RoundedCornerShape(10.px.dp)) + .background(Color(47, 51, 57)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Refer 1.", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 11.px.sp, + color = Color.White + ) + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + // VALUE + Column( + modifier = Modifier + .noRippleClickable(onClick = { onClick.invoke(2) }) + .fillMaxWidth() + .weight(1.15f) + .height(30.px.dp) + .clip(RoundedCornerShape(10.px.dp)) + .background(Color(47, 51, 57)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Refer 2.", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 11.px.sp, + color = Color.White + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewEnergyDetectView() { + Row { + EnergyDetectView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + title = "ENERGY \nDETECT" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EnergyVoltageView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EnergyVoltageView.kt new file mode 100644 index 0000000..08282db --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EnergyVoltageView.kt @@ -0,0 +1,185 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.const.Energies +import com.laseroptek.raman.const.PulseDurations +import com.laseroptek.raman.const.VoltageTable +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.ext.toVoltageMutableMap +import kotlinx.coroutines.flow.MutableStateFlow +import timber.log.Timber + + +@Composable +fun EnergyVoltageView( + voltageTable: MutableMap, MutableStateFlow> = VoltageTable.toVoltageMutableMap(), + selectedCalibPulseWidthIndex: Int = 0, + isVoltageSelectedElseReptition: Boolean = true, + selectedCalibVoltageIndex: Int = 0, + onClick: (Int) -> Unit = {} +) { + Row { + // L: Energy (Fluence) (J) + Column(modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(350.px.dp) + ) { + // Energy (Fluence) (J) - title + Row( + Modifier + .fillMaxWidth() + .height(22.px.dp) + .clip(RoundedCornerShape(topStart = 11.px.dp, topEnd = 11.px.dp)) + .background(Color.Black), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Energy (J)", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.White + ) + } + + // Energy (= Fluences, J) - value + for (i in 0..Energies.size -1) { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1f) + .border(0.5.px.dp, color = Color(209, 209, 209), shape = RectangleShape) + .background(Color.White), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = Energies[i].toString(), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + } + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + // R: Voltage (V) + Column(modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(350.px.dp) + ) { + // Voltage - title + Row( + Modifier + .fillMaxWidth() + .height(22.px.dp) + .clip(RoundedCornerShape(topStart = 11.px.dp, topEnd = 11.px.dp)) + .background(Color.Black), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Voltage (V)", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.White + ) + } + + // V - Voltage - value + for (i in 0..Energies.size -1 ) { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1f) + .border(0.5.px.dp, color = Color(209, 209, 209), shape = RectangleShape) + .padding(1.px.dp) + .background( + if (isVoltageSelectedElseReptition && i == selectedCalibVoltageIndex) + Color(255, 204, 0) + else + Color.White + ) + .noRippleClickable(onClick = { onClick.invoke(i) }), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Timber.d("-----------------------------------------------------------------") + Timber.d("selectedCalibPulseWidthIndex: ${selectedCalibPulseWidthIndex}") + Timber.d("PulseDurations(index): ${PulseDurations.get(selectedCalibPulseWidthIndex)}") + Timber.d("Energies(i): ${Energies.get(i)}") + + val key = Pair( + PulseDurations.get(selectedCalibPulseWidthIndex), + Energies.get(i) + ) + + val innerStateFlow = remember(key) { voltageTable[key] ?: MutableStateFlow(0) } + val voltage by innerStateFlow.collectAsState() + + Timber.d("-----------------------------------------------------------------") + Timber.d("voltage: ${voltage}") + Timber.d("") + + Text( + text = voltage.toString(), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewEnergyVoltageView() { + Row { + EnergyVoltageView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerScreen.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerScreen.kt new file mode 100644 index 0000000..c3648f8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerScreen.kt @@ -0,0 +1,1736 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.Intent +import android.content.res.Configuration +import android.provider.Settings +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.R +import com.laseroptek.raman.const.CMD +import com.laseroptek.raman.const.DEFAULT_GUIDE_BEAM +import com.laseroptek.raman.const.Energies +import com.laseroptek.raman.const.MAX_LAMP_COUNT +import com.laseroptek.raman.const.MAX_LIFETIME_DETECTOR +import com.laseroptek.raman.const.MAX_LIFETIME_HP +import com.laseroptek.raman.const.MAX_LIFETIME_LAMP +import com.laseroptek.raman.const.MAX_LIFETIME_WATER +import com.laseroptek.raman.const.MAX_OP_TIME +import com.laseroptek.raman.const.MAX_REFER2_VALUE +import com.laseroptek.raman.const.MIN_GUIDE_BEAM +import com.laseroptek.raman.const.PulseDurations +import com.laseroptek.raman.const.READ_WRITE +import com.laseroptek.raman.const.Repetitions +import com.laseroptek.raman.const.engineerModeButtonLists +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.serial.EnergyControl +import com.laseroptek.raman.data.model.serial.EnergyHandpiece +import com.laseroptek.raman.data.model.serial.EnergyMeasured +import com.laseroptek.raman.data.model.serial.LaserStatus +import com.laseroptek.raman.data.model.serial.Oven +import com.laseroptek.raman.data.model.serial.QSwitch +import com.laseroptek.raman.data.model.serial.getLaserStatusString +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.navigation.Routes +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.engineer.count.CountPopup +import com.laseroptek.raman.ui.screens.engineer.count.FloatPopup +import com.laseroptek.raman.ui.screens.engineer.dcd.DcdConfigPopup +import com.laseroptek.raman.ui.screens.engineer.default.DefaultConfirmPopup +import com.laseroptek.raman.ui.screens.engineer.energy.Refer1Popup +import com.laseroptek.raman.ui.screens.engineer.energy.Refer2Popup +import com.laseroptek.raman.ui.screens.engineer.export.ExportConfirmPopup +import com.laseroptek.raman.ui.screens.engineer.import.ImportConfirmPopup +import com.laseroptek.raman.ui.screens.engineer.load.LoadConfirmPopup +import com.laseroptek.raman.ui.screens.engineer.log.LogPopup +import com.laseroptek.raman.ui.screens.engineer.qswitch.QSwitchView +import com.laseroptek.raman.ui.screens.engineer.save.SaveConfirmPopup +import com.laseroptek.raman.ui.screens.engineer.serial.SerialNumberPopup +import com.laseroptek.raman.ui.screens.engineer.set.SetConfirmPopup +import com.laseroptek.raman.ui.screens.engineer.standby.EMStandByPopup +import com.laseroptek.raman.ui.screens.engineer.update.UpdateConfirmPopup +import com.laseroptek.raman.ui.screens.engineer.version.ProgramVersionPopup +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.decreaseVoltageTableValue +import com.laseroptek.raman.utils.ext.getValueFromVoltageTable +import com.laseroptek.raman.utils.ext.increaseVoltageTableValue +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.ext.setVoltageTable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EngineerScreen( + mainNavController: NavHostController = rememberNavController(), + paddingValues: PaddingValues = PaddingValues(), + mainViewModel: MainViewModel = hiltViewModel(), + engineerViewModel: EngineerViewModel = hiltViewModel(), + title: String = stringResource(R.string.engineer_mode) + ) { + val context = LocalContext.current // context For open system setting + val scope = rememberCoroutineScope() + + Timber.d("$$$ EngineerScreen $$$") + + // show Popups + var showUpdateConfirmPopup by remember { mutableStateOf(false) } + var showEmStandByPopup by remember { mutableStateOf(false) } + var showLogPopup by remember { mutableStateOf(false) } + var showSerialNumberPopup by remember { mutableStateOf(false) } + var showProgramVersionPopup by remember { mutableStateOf(false) } + var showDcdConfigPopup by remember { mutableStateOf(false) } + var showDefaultConfirmPopup by remember { mutableStateOf(false) } + var showSaveConfirmPopup by remember { mutableStateOf(false) } + var showLoadConfirmPopup by remember { mutableStateOf(false) } + var showLampTotalCountPopup by remember { mutableStateOf(false) } + var showDeviceOpTimePopup by remember { mutableStateOf(false) } + var showImportConfirmPopup by remember { mutableStateOf(false) } + var showExportConfirmPopup by remember { mutableStateOf(false) } + var showSetConfirmPopup by remember { mutableStateOf(false) } + + // show Popups - LifeTime (show) + var showLifeTimeLampCountPopup by remember { mutableStateOf(false) } + var showLifeTypeHP5x5Popup by remember { mutableStateOf(false) } + var showLifeTimeHP7x7Popup by remember { mutableStateOf(false) } + var showLifeTimeHP10x10Popup by remember { mutableStateOf(false) } + var showLifeTimeHP12x12Popup by remember { mutableStateOf(false) } + var showLifeTimeHP3x15Popup by remember { mutableStateOf(false) } + var showLifeTimeDetectorPopup by remember { mutableStateOf(false) } + var showLifeTimeWaterPopup by remember { mutableStateOf(false) } + + // show Popups - Temp (show) + var showTempKTPPopup by remember { mutableStateOf(false) } + var showTempChamber1Popup by remember { mutableStateOf(false) } + var showTempChamber2Popup by remember { mutableStateOf(false) } + var showTempBaseplatePopup by remember { mutableStateOf(false) } + var showTempWaterPopup by remember { mutableStateOf(false) } + + // show Popups - Energy Detect + var showRefer1Popup by remember { mutableStateOf(false) } + var showRefer2Popup by remember { mutableStateOf(false) } + + // mainViewModel vars + val handPiece by mainViewModel.handPiece.collectAsState() + val laserCount by mainViewModel.laserCount.collectAsState() + val lampCount by mainViewModel.lampCount.collectAsState() + val dcdType by mainViewModel.dcdType.collectAsState() + val opTimeHour by mainViewModel.opTimeHour.collectAsState() + val temperatureRead by mainViewModel.temperature.collectAsState() + val energyMeasured by mainViewModel.energyMeasured.collectAsState() + val energyDetectRefer2 by mainViewModel.energyDetectRefer2.collectAsState() + val version by mainViewModel.version.collectAsState() + val productSerialList by mainViewModel.productSerialList.collectAsState() + val laserHandSerialList by mainViewModel.laserHandSerialList.collectAsState() + val powerSupplySerialList by mainViewModel.powerSupplySerialList.collectAsState() + //val guideBeam by mainViewModel.guideBeam.collectAsState() + val guideBeamMin by mainViewModel.guideBeamMin.collectAsState() + val guideBeamMax by mainViewModel.guideBeamMax.collectAsState() + val qSwitch by mainViewModel.qSwitch.collectAsState() + val energyHandpiece by mainViewModel.energyHandpiece.collectAsState() + + // engineerViewModel vars (1) + val isVoltageSelectedElseReptition by engineerViewModel.isVoltageSelectedElseReptition.collectAsState() + val selectedCalibRepetitionIndex by engineerViewModel.selectedCalibRepetitionIndex.collectAsState() + val selectedCalibVoltageIndex by engineerViewModel.selectedCalibVoltageIndex.collectAsState() + val selectedCalibPulseWidthIndex by engineerViewModel.selectedCalibPulseWidthIndex.collectAsState() + + // engineerViewModel vars (2) + val voltageTable by engineerViewModel.voltageTable.collectAsState() + val laserStatus by engineerViewModel.laserStatus.collectAsState() + val lifeTime by engineerViewModel.lifeTime.collectAsState() + + val temperatureWrite by engineerViewModel.temperature_write.collectAsState() + val energyMeasuredWrite by engineerViewModel.energyMeasuredWrite.collectAsState() + + LaunchedEffect(Unit) { + Timber.d("$$$ EngineerScreen LaunchedEffect $$$") + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + } + } + + FullScreen( + contentAlignment= Alignment.TopCenter, + backgroundColor = Color.Transparent + ) { + + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(Color.Transparent), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color(255, 255, 255), + 0.5f to Color(235, 235, 235), + 1.0f to Color(216, 216, 216) + ) + ) + ) + .padding( + top = 20.px.dp, bottom = 20.px.dp, start = 40.px.dp, end = 40.px.dp + ) + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Top + ) { + //titleBar + EngineerTitltBarView( + title = title, + height = 36.px.dp, + onCloseClick = { + Timber.d("onCloseClick") + + // update (load Voltage Table changed from Preference) + scope.launch { + mainViewModel.loadVoltageTableFromPreference() + } + + mainNavController.navigate(Routes.Config.route) + }, + onOSSetupClick = { + try { + context.startActivity(Intent(Settings.ACTION_SETTINGS)) + } catch (e: Exception) { + Timber.e(e, "Could not open ACTION_SETTINGS") + // Fallback: Show a toast or other message if opening settings fails + Toast.makeText( + context, + "Could not open ACTION_SETTINGS. Please navigate manually.", + Toast.LENGTH_LONG + ).show() + } + } + ) + + Spacer(modifier = Modifier.height(10.px.dp)) + + // body + Row(modifier = Modifier + .fillMaxSize(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // left + Column(modifier = Modifier + .fillMaxSize() + .padding(end = 0.px.dp) + .weight(1f) + ) { + // left - left side + Row(modifier = Modifier + .fillMaxSize() + .weight(1f) + ) { + // left - left pannel + Column(modifier = Modifier + .width(388.px.dp) + .fillMaxHeight() + //.background(Color.Yellow) + ) { + // Repetion And Voltage + Row { + // L: Repetion (Hz) + RepetitionView( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + selectedCalibRepetitionIndex = selectedCalibRepetitionIndex, + isVoltageSelectedElseReptition = isVoltageSelectedElseReptition, + title = stringResource(R.string.repetition_title), + onClick = { + engineerViewModel.setIsVoltageSelectedElseReptition(false) + } + ) + + Spacer(modifier = Modifier.width(10.px.dp)) + + // R: Voltage (V) + VoltageView( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + laserStatus = laserStatus, + title = stringResource(R.string.voltage_title) + ) + } + Spacer(modifier = Modifier.height(8.px.dp)) + + // left - pulse-width (ms) + PulseWidthView( + selectedCalibPulseWidthIndex = selectedCalibPulseWidthIndex, + onClick = { index -> + Timber.d("onClickPulseWidth: $index") + engineerViewModel.setSelectedCalibPulseWidthIndex(index) + } + ) + Spacer(modifier = Modifier.height(8.px.dp)) + + // left - reptition N voltage select + EnergyVoltageView( + voltageTable = voltageTable, + isVoltageSelectedElseReptition = isVoltageSelectedElseReptition, + selectedCalibVoltageIndex = selectedCalibVoltageIndex, + selectedCalibPulseWidthIndex = selectedCalibPulseWidthIndex, + onClick = { index -> + Timber.d("onClickVoltage: $index") + + engineerViewModel.setIsVoltageSelectedElseReptition(true) + engineerViewModel.setSelectedCalibVoltageIndex(index) + }, + ) + Spacer(modifier = Modifier.height(8.px.dp)) + + // left - // left - reptition N voltage change + RepetitionVoltageControlView( + onClickUp = { + Timber.d("[EM] onClickUp") + Timber.d("[EM] selectedCalibPulseWidthIndex: ${selectedCalibPulseWidthIndex}") + Timber.d("[EM] selectedCalibVoltageIndex: ${selectedCalibVoltageIndex}") + // increase volume by 1 + if (isVoltageSelectedElseReptition) { + val selectedPulseWidth = PulseDurations[selectedCalibPulseWidthIndex] + increaseVoltageWithCascade( + voltageTable = voltageTable, + pulseWidth = selectedPulseWidth, + selectedVoltageIndex = selectedCalibVoltageIndex, + increament = 1 + ) + } else if (selectedCalibRepetitionIndex < Repetitions.size - 1) { + engineerViewModel.setSelectedCalibRepetitionIndex(selectedCalibRepetitionIndex + 1) + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } else { + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } + }, + onClickUpLongPress = { + Timber.d("[EM] onClickUp") + Timber.d("[EM] selectedCalibPulseWidthIndex: ${selectedCalibPulseWidthIndex}") + Timber.d("[EM] selectedCalibVoltageIndex: ${selectedCalibVoltageIndex}") + // increase volume by 10 + if (isVoltageSelectedElseReptition) { + val selectedPulseWidth = PulseDurations[selectedCalibPulseWidthIndex] + increaseVoltageWithCascade( + voltageTable = voltageTable, + pulseWidth = selectedPulseWidth, + selectedVoltageIndex = selectedCalibVoltageIndex, + increament = 10 + ) + } else if (selectedCalibRepetitionIndex < Repetitions.size - 1) { + engineerViewModel.setSelectedCalibRepetitionIndex(selectedCalibRepetitionIndex + 1) + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } else { + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } + }, + onClickDown = { + Timber.d("[EM] onClickDown") + Timber.d("[EM] selectedCalibPulseWidthIndex: ${selectedCalibPulseWidthIndex}") + Timber.d("[EM] selectedCalibVoltageIndex: ${selectedCalibVoltageIndex}") + if (isVoltageSelectedElseReptition) { + val selectedPulseWidth = PulseDurations[selectedCalibPulseWidthIndex] + decreaseVoltageWithCascade( + voltageTable = voltageTable, + pulseWidth = selectedPulseWidth, + selectedVoltageIndex = selectedCalibVoltageIndex, + decreament = 1 + ) + } else if (selectedCalibRepetitionIndex > 0) { + engineerViewModel.setSelectedCalibRepetitionIndex(selectedCalibRepetitionIndex - 1) + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } else { + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } + }, + onClickDownLongPress = { + Timber.d("[EM] onClickDown") + Timber.d("[EM] selectedCalibPulseWidthIndex: ${selectedCalibPulseWidthIndex}") + Timber.d("[EM] selectedCalibVoltageIndex: ${selectedCalibVoltageIndex}") + if (isVoltageSelectedElseReptition) { + val selectedPulseWidth = PulseDurations[selectedCalibPulseWidthIndex] + decreaseVoltageWithCascade( + voltageTable = voltageTable, + pulseWidth = selectedPulseWidth, + selectedVoltageIndex = selectedCalibVoltageIndex, + decreament = 10 + ) + } else if (selectedCalibRepetitionIndex > 0) { + engineerViewModel.setSelectedCalibRepetitionIndex(selectedCalibRepetitionIndex - 1) + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } else { + Timber.d("selectedCalibRepetitionIndex: ${selectedCalibRepetitionIndex}") + } + }, + onClickDtoV = { + Timber.d("onClickDtoV : emVoltageTable -> voltageTable") + Timber.d("[EM] selectedCalibPulseWidthIndex: ${selectedCalibPulseWidthIndex}") + Timber.d("[EM] selectedCalibVoltageIndex: ${selectedCalibVoltageIndex}") + val key: Pair = Pair( + PulseDurations[selectedCalibPulseWidthIndex], + Energies[selectedCalibVoltageIndex] + ) + val v = laserStatus.voltage + voltageTable.setVoltageTable(key, v) + }, + onClickVtoD = { + Timber.d("onClickVtoD : emVoltageTable -> voltageTable") + Timber.d("[EM] selectedCalibPulseWidthIndex: ${selectedCalibPulseWidthIndex}") + Timber.d("[EM] selectedCalibVoltageIndex: ${selectedCalibVoltageIndex}") + val key: Pair = Pair( + PulseDurations[selectedCalibPulseWidthIndex], + Energies[selectedCalibVoltageIndex] + ) + val v = voltageTable.getValueFromVoltageTable(key) ?: 0 + engineerViewModel.setLaserStatus( + LaserStatus( + voltage = v, + onTime = PulseDurations[selectedCalibPulseWidthIndex] + ) + ) + }, + ) + } + + // middle - os N temp + Column(modifier = Modifier + .fillMaxSize() + .padding(start = 20.px.dp) + .weight(1f) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(10.px.dp)) + .border( + width = 1.px.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(10.px.dp) + ) + .background(Color.White), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(10.px.dp)) + + // Q Switch (main) + QSwitchView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + qSwitch = qSwitch, + title = "Q-S/W (main)", + onClick = { qSwitch -> + Timber.d("onClick > Q-S/W (main)") + mainViewModel.setQSwitch(qSwitch) + scope.launch { + mainViewModel.saveQSwitchToPreference() + } + } + ) + + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Q-S/W (sub) + QSwitchView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + qSwitch = qSwitch, //qSwitchSub, + title = "Q-S/W (sub)", + onClick = { qSwitch -> + Timber.d("onClick > Q-S/W (sub)") + //mainViewModel.setQSwitchSub(qSwitch) + mainViewModel.setQSwitch(qSwitch) + scope.launch { + mainViewModel.saveQSwitchToPreference() + } + } + ) + + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Energy Detect + EnergyDetectView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + title = "ENERGY \nDETECT", + onClick = { no -> + Timber.d("onClick > Energy Detect") + when (no) { + 1 -> showRefer1Popup = true + 2 -> showRefer2Popup = true + else -> {} + } + } + ) + + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // DCD + DcdView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + dcdType = dcdType, + title = "DCD", + onClick = { + showDcdConfigPopup = true + } + ) + + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Guide Beam + GuideBeamView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + title = "Guide Beam", + minValue = guideBeamMin, + maxValue = guideBeamMax, + onClick = { state -> + scope.launch { + mainViewModel.onGuideBeamChanged(state) + } + } + ) + + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Lamp Total Count + CountItemView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + count = lampCount, + title = "Lamp \nTotal Count", + onClick = { + showLampTotalCountPopup = true + } + ) + + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Device Operation Time + HourItemView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + hour = opTimeHour.toInt(), + title = "Device \nOperation Time", + onClick = { + showDeviceOpTimePopup = true + } + ) + + Spacer(modifier = Modifier.height(10.px.dp)) + } + + Spacer(modifier = Modifier.height(8.px.dp)) + + // LifeTime View + LifeTimeView( + lifeTime = lifeTime, + onClick = { index -> + when (index) { + 0 -> { + Timber.d("onClick > LifeTime") + showLifeTimeLampCountPopup = true + } + 1 -> { + Timber.d("onClick > HP5x5") + showLifeTypeHP5x5Popup = true + } + 2 -> { + Timber.d("onClick > HP7x7") + showLifeTimeHP7x7Popup = true + } + 3 -> { + Timber.d("onClick > HP10x10") + showLifeTimeHP10x10Popup = true + } + 4 -> { + Timber.d("onClick > HP12x12") + showLifeTimeHP12x12Popup = true + } + 5 -> { + Timber.d("onClick > HP3x15") + showLifeTimeHP3x15Popup = true + } + 6 -> { + Timber.d("onClick > HP3x15") + showLifeTimeDetectorPopup = true + } + 7 -> { + Timber.d("onClick > HP3x15") + showLifeTimeWaterPopup = true + } + else -> {} + } + + } + ) + } + } + + Spacer(modifier = Modifier.height(10.px.dp)) + + // bottom Buttons + Row(modifier = Modifier + .fillMaxWidth() + .height(32.px.dp), + //.background(Color.Yellow), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + engineerModeButtonLists.forEachIndexed { index, data -> + IconButton( + onClick = { + Timber.d("Perform action when button is clicked: %d", index) + when(index) { + 0 -> { + Timber.d("onUpdateClick") + showUpdateConfirmPopup = true + } + 1 -> { + Timber.d("onDefaultClick") + showDefaultConfirmPopup = true + } + 2 -> { + Timber.d("onSaveClick") + showSaveConfirmPopup = true + } + 3 -> { + Timber.d("onLoadClick") + showLoadConfirmPopup = true + } + 4 -> { + Timber.d("onImportClick") + showImportConfirmPopup = true + } + 5 -> { + Timber.d("onExportClick") + showExportConfirmPopup = true + } + 6 -> { + Timber.d("onLogClick") + scope.launch { + mainViewModel.fetchSerialLog() + } + showLogPopup = true + } + } + }, + modifier = Modifier + .size(98.px.dp, 32.px.dp) + .clip(RoundedCornerShape(percent = 50)), + colors = IconButtonDefaults.iconButtonColors( + contentColor = data.contentColor, // Icon and Text color + containerColor = data.containerColor, // Background color + disabledContentColor = data.contentColor, // Icon color when disabled + disabledContainerColor = data.containerColor // Background color when disabled + ) + ) { + Box(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier.align(Alignment.Center), // This now works correctly. + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = data.image), + contentDescription = null, + modifier = Modifier.size(14.px.dp), + contentScale = ContentScale.Fit, + colorFilter = ColorFilter.tint(data.contentColor) + ) + + // Spacer + Spacer(modifier = Modifier.width(6.px.dp)) + + // Text + Text( + text = stringResource(data.title), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = data.contentColor + ) + + Spacer(modifier = Modifier.width(6.px.dp)) + } + } + } + } + } + } + + // right + Column(modifier = Modifier + .width(408.px.dp) + .fillMaxHeight() + .padding(start = 20.px.dp) + ) { + // Temperature View + TemperatureView( + temperatureRead = temperatureRead, + temperatureWrite = temperatureWrite, + onClick = { index -> + when (index) { + 0 -> { + Timber.d("onClick > showTempKTPPopup") + showTempKTPPopup = true + } + 1 -> { + Timber.d("onClick > showTempChamber1Popup") + showTempChamber1Popup = true + } + 2 -> { + Timber.d("onClick > showTempChamber2Popup") + showTempChamber2Popup = true + } + 3 -> { + Timber.d("onClick > showTempBaseplatePopup") + showTempBaseplatePopup = true + } + 4 -> { + Timber.d("onClick > showTempWaterPopup") + showTempWaterPopup = true + } + else -> {} + } + + }, + ) + Spacer(modifier = Modifier.height(8.px.dp)) + + VersionView( + version = version, + productSerialList = productSerialList, + laserHandSerialList = laserHandSerialList, + powerSupplySerialList = powerSupplySerialList, + onClick = { idx -> + when (idx) { + 0 -> { + Timber.d("onClick > showSerialNumberPopup") + showSerialNumberPopup = true + } + else -> { + Timber.d("onClick > showProgramVersionPopup") + showProgramVersionPopup = true + } + } + } + ) + Spacer(modifier = Modifier.height(8.px.dp)) + + // set Button + SetButton( + onClick = { + Timber.d("onSetClick") + //onSetClick.invoke() + showSetConfirmPopup = true + + //mainViewModel.setVoltageTable() + //Voltage Table를 제외한 다른 설정 값 저장 (3.31) + + // send qSwitch + //mainViewModel.txPacket(READ_WRITE.WRITE, CMD.Q_SWITCH, mainViewModel.qSwitch.value) + }, + title = stringResource(R.string.set) + ) + Spacer(modifier = Modifier.height(8.px.dp)) + + // StandBy Button + StandByButton( + onClick = { + Timber.d("onEmStandByClick: ${laserStatus}") + + if (handPiece.type == 0) { + Toast.makeText( + context, + "NO HAND-PIECE recognized", + Toast.LENGTH_SHORT + ).show() + return@StandByButton + } + + laserStatus.laserStatus = 0x52 // ready + + // repetition + val repetition = Repetitions.get( + selectedCalibRepetitionIndex + ) + laserStatus.frequence = repetition + + // pulseWith + val pulseWith = PulseDurations.get( + selectedCalibPulseWidthIndex + ) + laserStatus.onTime = pulseWith + + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, laserStatus) + + Toast.makeText( + context, + "[TX][onTime(ms): %f, frequence(Hz): %f) -> V(V): %d)]".format( + laserStatus.onTime, laserStatus.frequence, laserStatus.voltage + ), + Toast.LENGTH_LONG + ).show() + + showEmStandByPopup = true + }, + title = getLaserStatusString(laserStatus.laserStatus) + ) + } + } + } + } + + //////////////////////////////////////////////////////////////////////////////////////////// + // Popups + if (showSerialNumberPopup) { + SerialNumberPopup( + title = stringResource(R.string.serial_number), + productSerialList = productSerialList.toMutableList(), + laserHandSerialList = laserHandSerialList.toMutableList(), + powerSupplySerialList = powerSupplySerialList.toMutableList(), + onClick = { ok, productSerialList, laserHandSerialList, powerSupplySerialList -> + if (ok) { + Timber.d("onClick > productSerialList: $productSerialList") + Timber.d("onClick > laserHandSerialList: $laserHandSerialList") + Timber.d("onClick > powerSupplySerialList: $powerSupplySerialList") + + mainViewModel.setProductSerialList(productSerialList) + mainViewModel.setLaserHandSerialList(laserHandSerialList) + mainViewModel.setPowerSupplySerialList(powerSupplySerialList) + + // need to save to preference + scope.launch { + mainViewModel.saveProductSerialListToPreference() + mainViewModel.saveLaserHandSerialListToPreference() + mainViewModel.savePowerSupplySerialListToPreference() + } + } + showSerialNumberPopup = false + } + ) + } + + else if (showProgramVersionPopup) { + ProgramVersionPopup( + title = stringResource(R.string.program_version), + version = mainViewModel.version, + onClick = { + showProgramVersionPopup = false + } + ) + } + + else if (showDcdConfigPopup) { + DcdConfigPopup( + mainViewModel = mainViewModel, + //showDcdConfigPopup = showDcdConfigPopup, + //dcdType = mainViewModel.dcdType, + onClick = { + showDcdConfigPopup = false + } + ) + } + + else if (showLampTotalCountPopup) { + CountPopup( + //mainViewModel = mainViewModel, + title = stringResource(R.string.lamp_total_count_title), + count = lampCount, + maxValue = MAX_LAMP_COUNT, + onClick = { ok, value -> + if (ok) { + mainViewModel.setLampCount( value ) + scope.launch { + mainViewModel.saveLampCountToPreference() + } + } + showLampTotalCountPopup = false + } + ) + } + + else if (showDeviceOpTimePopup){ + CountPopup( + title = stringResource(R.string.device_operation_time_title), + count = opTimeHour.toInt(), + maxValue = MAX_OP_TIME, + onClick = { ok, value -> + if (ok) { + mainViewModel.setOpTimeHour( value.toLong() ) + scope.launch { + mainViewModel.saveOpTimeHourToPreference() + } + } + showDeviceOpTimePopup = false + } + ) + } + + // Life Time { + else if (showLifeTimeLampCountPopup){ + CountPopup( + title = stringResource(R.string.lifetime_lamp_count_title), + count = lifeTime.lamp, + maxValue = MAX_LIFETIME_LAMP, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > lampCount: $value") + + mainViewModel.setLifeTime( + lifeTime.copy( + lamp = value + ) + ) + + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTimeLampCountPopup = false + } + ) + } + + else if (showLifeTypeHP5x5Popup){ + CountPopup( + title = stringResource(R.string.lifetime_hp5x5_count_title), + count = lifeTime.hp5x5, + maxValue = MAX_LIFETIME_HP, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > hp5x5: $value") + mainViewModel.setLifeTime( + lifeTime.copy( + hp5x5 = value + ) + ) + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTypeHP5x5Popup = false + } + ) + } + + else if (showLifeTimeHP7x7Popup){ + CountPopup( + title = stringResource(R.string.lifetime_hp7x7_count_title), + count = lifeTime.hp7x7, + maxValue = MAX_LIFETIME_HP, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > hp7x7: $value") + mainViewModel.setLifeTime( + lifeTime.copy( + hp7x7 = value + ) + ) + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTimeHP7x7Popup = false + } + ) + } + + else if (showLifeTimeHP10x10Popup){ + CountPopup( + title = stringResource(R.string.lifetime_hp10x10_count_title), + count = lifeTime.hp10x10, + maxValue = MAX_LIFETIME_HP, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > hp10x10: $value") + mainViewModel.setLifeTime( + lifeTime.copy( + hp10x10 = value + ) + ) + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTimeHP10x10Popup = false + } + ) + } + + else if (showLifeTimeHP12x12Popup){ + CountPopup( + title = stringResource(R.string.lifetime_hp12x12_count_title), + count = lifeTime.hp12x12, + maxValue = MAX_LIFETIME_HP, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > hp12x12: $value") + mainViewModel.setLifeTime( + lifeTime.copy( + hp12x12 = value + ) + ) + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTimeHP12x12Popup = false + } + ) + } + + else if (showLifeTimeHP3x15Popup){ + CountPopup( + title = stringResource(R.string.lifetime_hp3x15_count_title), + count = lifeTime.hp3x15, + maxValue = MAX_LIFETIME_HP, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > hp3x15: $value") + mainViewModel.setLifeTime( + lifeTime.copy( + hp3x15 = value + ) + ) + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTimeHP3x15Popup = false + } + ) + } + else if (showLifeTimeDetectorPopup){ + CountPopup( + title = stringResource(R.string.lifetime_detector_count_title), + count = lifeTime.detector, + maxValue = MAX_LIFETIME_DETECTOR, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > detector: $value") + mainViewModel.setLifeTime( + lifeTime.copy( + detector = value + ) + ) + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTimeDetectorPopup = false + } + ) + } + else if (showLifeTimeWaterPopup){ + CountPopup( + title = stringResource(R.string.lifetime_water_count_title), + count = lifeTime.water, + maxValue = MAX_LIFETIME_WATER, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > water: $value") + mainViewModel.setLifeTime( + lifeTime.copy( + water = value + ) + ) + scope.launch { + mainViewModel.saveLifeTimeToPreference() + } + } + showLifeTimeWaterPopup = false + } + ) + } + // } Life Time + + // Temp { + else if (showTempKTPPopup){ + FloatPopup( + title = stringResource(R.string.ktp_count_title), + count = temperatureWrite.ktp, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > ktp: $value") + mainViewModel.setTemperatureWrite( + temperatureWrite.copy( + ktp = value.toFloat() + ) + ) + + // send KTP (OVEN) + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.OVEN, + Oven( + type = 0x41, + ktp = temperatureWrite.ktp + ) + ) + + scope.launch { + mainViewModel.saveTemeratureWriteToPreference() + } + } + showTempKTPPopup = false + } + ) + } + + else if (showTempChamber1Popup){ + FloatPopup( + title = stringResource(R.string.chamber1_count_title), + count = temperatureWrite.chamber1, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > chamber1: $value") + + mainViewModel.setTemperatureWrite( + temperatureWrite.copy( + chamber1 = value.toFloat() + ) + ) + scope.launch { + mainViewModel.saveTemeratureWriteToPreference() + } + } + showTempChamber1Popup = false + } + ) + } + + else if (showTempChamber2Popup){ + FloatPopup( + title = stringResource(R.string.chamber2_count_title), + count = temperatureWrite.chamber2, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > chamber2: $value") + mainViewModel.setTemperatureWrite( + temperatureWrite.copy( + chamber2 = value.toFloat() + ) + ) + scope.launch { + mainViewModel.saveTemeratureWriteToPreference() + } + } + showTempChamber2Popup = false + } + ) + } + + else if (showTempWaterPopup){ + FloatPopup( + title = stringResource(R.string.water_count_title), + count = temperatureWrite.water, + onClick = { ok, value -> + if (ok) { + mainViewModel.setTemperatureWrite( + temperatureWrite.copy( + water = value.toFloat() + ) + ) + scope.launch { + mainViewModel.saveTemeratureWriteToPreference() + } + } + showTempWaterPopup = false + } + ) + } + + else if (showTempBaseplatePopup){ + FloatPopup( + title = stringResource(R.string.baseplate_count_title), + count = temperatureWrite.basePlate, + onClick = { ok, value -> + if (ok) { + Timber.d("onClick > basePlate: $value") + mainViewModel.setTemperatureWrite( + temperatureWrite.copy( + basePlate = value.toFloat() + ) + ) + scope.launch { + mainViewModel.saveTemeratureWriteToPreference() + } + } + showTempBaseplatePopup = false + } + ) + } + // } Temp + + else if (showDefaultConfirmPopup) { + DefaultConfirmPopup( + onClick = { ok -> + if (ok) { + engineerViewModel.factoryDefault() + mainViewModel.setGuideBeamMin(MIN_GUIDE_BEAM) + mainViewModel.setGuideBeamMax(DEFAULT_GUIDE_BEAM) + mainViewModel.setQSwitch(QSwitch()) + mainViewModel.setQSwitchSub(QSwitch()) + } + showDefaultConfirmPopup = false + } + ) + } + + else if (showUpdateConfirmPopup) { + UpdateConfirmPopup( + onClick = { ok -> + if (ok) { + Timber.d("onClick - UpdateConfirmPopup") + scope.launch { + mainViewModel.emitUpdateApkEvent() + } + } + showUpdateConfirmPopup = false + } + ) + } + + else if (showSaveConfirmPopup) { + SaveConfirmPopup( + onClick = { ok -> + if (ok) { + // save voltage to preference + Timber.d("onClick - SaveConfirmPopup") + scope.launch { + engineerViewModel.saveToPreference() + + mainViewModel.saveGuideBeamMinToPreference() + mainViewModel.saveGuideBeamMaxToPreference() + } + } + showSaveConfirmPopup = false + } + ) + } + + else if (showLoadConfirmPopup) { + LoadConfirmPopup( + onClick = { ok -> + if (ok) { + Timber.d("onClick - LoadConfirmPopup") + scope.launch { + engineerViewModel.loadFromPreference() + + mainViewModel.loadGuideBeamMinFromPreference() + mainViewModel.loadGuideBeamMaxFromPreference() + mainViewModel.loadQSwitchFromPreference() + } + } + showLoadConfirmPopup = false + } + ) + } + + else if (showImportConfirmPopup) { + ImportConfirmPopup( + onClick = { ok -> + if (ok) { + Timber.d("onClick - ImportConfirmPopup") + scope.launch { + engineerViewModel.loadFromPreference() + } + } + showImportConfirmPopup = false + } + ) + + } + + else if (showExportConfirmPopup) { + ExportConfirmPopup( + onClick = { ok -> + Timber.d("onClick - ImportConfirmPopup $ok") + + // save others + scope.launch { + engineerViewModel.saveToPreference() + + mainViewModel.saveOpTimeHourToPreference() + mainViewModel.saveQSwitchToPreference() + mainViewModel.saveGuideBeamMinToPreference() + mainViewModel.saveGuideBeamMaxToPreference() + mainViewModel.saveLampCountToPreference() + mainViewModel.saveSprayDcdListToPreference() + mainViewModel.savePresetListToPreference() + mainViewModel.saveLifeTimeToPreference() + mainViewModel.saveTemeratureWriteToPreference() + mainViewModel.saveEnergyMeasuredWriteToPreference() + mainViewModel.saveEnergyRefer2ToPreference() + mainViewModel.saveProductSerialListToPreference() + mainViewModel.savePowerSupplySerialListToPreference() + mainViewModel.saveLaserHandSerialListToPreference() + } + + showExportConfirmPopup = false + } + ) + } + + else if (showSetConfirmPopup) { + SetConfirmPopup( + onClick = { ok -> + if (ok) { + Timber.d("onClick - SetConfirmPopup $ok") + // 1. qSwitch send + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.Q_SWITCH, + qSwitch + ) + + // 2. Oven (ktp write) send + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.OVEN, + Oven( + type = 0x41, + ktp = temperatureWrite.ktp + ) + ) + } + showSetConfirmPopup = false + } + ) + } + + else if (showLogPopup) { + LogPopup( + //serialLogs = serialLogs, + serialLogList = mainViewModel.serialLogList, + onClick = { + showLogPopup = false + } + ) + } + + else if (showEmStandByPopup) { + EMStandByPopup( + title = stringResource(R.string.stand_by_title), + description = stringResource(R.string.stand_by_message), + laserStatus = engineerViewModel.laserStatus, + laserCount = laserCount, + onClick = { + laserStatus.laserStatus = 0x53 // standBy + Timber.d("onEmStandByClick: ${laserStatus}") + + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, laserStatus) + + Toast.makeText( + context, + "[TX][onTime(ms): %f, frequence(Hz): %f) -> V(V): %d)]".format( + laserStatus.onTime, laserStatus.frequence, laserStatus.voltage + ), + Toast.LENGTH_LONG + ).show() + + showEmStandByPopup = false + } + ) + } + + else if (showRefer1Popup) { + Timber.d("energyMeasured.measured: ${energyMeasured.measured}") + + // send hand piece mount + mainViewModel.txPacket( + READ_WRITE.READ, + CMD.ENERGY_DETECT, + EnergyHandpiece(type = 0x02, status = 0x41) + ) + + Refer1Popup( + hpType = handPiece.type, + measured = energyMeasured.measured, + measuredWrite = energyMeasuredWrite.measured, + onClickRead = { + // start detecting + // + // send start (energy control) + // ready: send start + + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + } else if (energyHandpiece.status == 0x47) { + val voltage = mainViewModel.getVoltage(10f, 10f) + Timber.d("onClickRead voltage: $voltage") + + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.ENERGY_DETECT, + EnergyControl( + type = 0x03, + status = 0x41, // 'A': Start + voltage = voltage + ) + ) + } else { + Toast.makeText( + context, + "Handpiece mount error. Try later", + Toast.LENGTH_LONG + ).show() + } + }, + onClickCancel = { + showRefer1Popup = false + }, + onClickSet = { v -> + // write measured value + mainViewModel.setEnergyMeasuredWrite( + EnergyMeasured( + measured = v + ) + ) + + scope.launch { + mainViewModel.saveEnergyMeasuredWriteToPreference() + } + + /* + Toast.makeText( + context, + "Set Measured Write", + Toast.LENGTH_SHORT + ).show() + */ + } + ) + } + + else if (showRefer2Popup) { + Refer2Popup( + refer2 = energyDetectRefer2, + maxValue = MAX_REFER2_VALUE, // 100% (=1) + onClick = { ok, refer2 -> + if (ok) { + mainViewModel.setEnergyDetectRefer2( + refer2 + ) + + scope.launch { + mainViewModel.saveEnergyRefer2ToPreference() + } + + /* + Toast.makeText( + context, + "Refer2(good: ${refer2.good}, acceptable: ${refer2.acceptable}, bad: ${refer2.bad}) Set", + Toast.LENGTH_SHORT + ).show() + */ + } else { + showRefer2Popup = false + } + } + ) + } + } +} + +private fun increaseVoltageWithCascade( + voltageTable: MutableMap, MutableStateFlow>, + pulseWidth: Float, + selectedVoltageIndex: Int, + increament: Int = 1 +) { + //1. Get the current key and voltage + val currentEnergy = Energies[selectedVoltageIndex] + val currentKey = Pair(pulseWidth, currentEnergy) + val currentVoltage = voltageTable[currentKey]?.value ?: 0 + + // Ensure we don't try to increase past the system limit + if (currentVoltage >= 990) { + Timber.d("[EM] Cannot increase voltage for $currentKey, already at or above max (990).") + return + } + + // 2. Safely get the voltage of the next energy level + val nextVoltage = if (selectedVoltageIndex < Energies.size - 1) { + val nextEnergy = Energies[selectedVoltageIndex + 1] + val nextKey = Pair(pulseWidth, nextEnergy) + voltageTable[nextKey]?.value ?: 0 + } else { + // If we are already at the last item, there is no "next" voltage to worry about. + // We can use a value higher than any possible voltage to allow the increase. + 1000 // Effectively infinity + } + + // 3. Simple Case: If we can increase the current voltage without equalling or exceeding the next one. + if (currentVoltage + increament < nextVoltage) { + Timber.d("[EM] Simple increase for $currentKey") + voltageTable.increaseVoltageTableValue(currentKey, increament) + return // We are done + } + + // last key pair + val lastSelectedCalibVoltageIndex = Energies[Energies.size -1] + val lastKey = Pair(pulseWidth, lastSelectedCalibVoltageIndex) + val lastVoltage = voltageTable[lastKey]?.value ?: 0 + + if (lastVoltage + increament > 990) { + Timber.d("[EM] Cannot increase voltage for $lastKey, already at or above max (990).") + return + } + + // 4. Cascade Case: If increasing the voltage would conflict. + // We must increase the voltage for the current level and all subsequent levels by 1. + Timber.d("[EM] Cascade increase starting from index $selectedVoltageIndex") + + for (i in selectedVoltageIndex until Energies.size) { + val energyForIndex = Energies[i] + val keyToUpdate = Pair(pulseWidth, energyForIndex) + val voltageToUpdate = voltageTable[keyToUpdate]?.value ?: 0 + + // Ensure we don't increase any value in the cascade beyond the max limit + if (voltageToUpdate < 990) { + voltageTable.increaseVoltageTableValue(keyToUpdate, increament) + } else { + // If any value in the chain is already at max, we can't push it further. + // We should stop the entire operation to maintain the ascending order integrity. + Timber.w("[EM] Cascade stopped at $keyToUpdate, value is already at max voltage (990).") + // It might be better to revert the changes or show a toast here, but for now, we just stop. + break + } + } +} + +private fun decreaseVoltageWithCascade( + voltageTable: MutableMap, MutableStateFlow>, + pulseWidth: Float, + selectedVoltageIndex: Int, + decreament: Int = 1 +) { + // 1. Get the current key and voltage + val currentEnergy = Energies[selectedVoltageIndex] + val currentKey = Pair(pulseWidth, currentEnergy) + val currentVoltage = voltageTable[currentKey]?.value ?: 0 + + // Ensure we don't decrease below a minimum (e.g., 0) + if (currentVoltage <= 0) { + Timber.d("[EM] Cannot decrease voltage for $currentKey, already at or below 0.") + return + } + + // 2. Safely get the voltage of the PREVIOUS energy level + val previousVoltage = if (selectedVoltageIndex > 0) { + val previousEnergy = Energies[selectedVoltageIndex - 1] + val previousKey = Pair(pulseWidth, previousEnergy) + voltageTable[previousKey]?.value ?: 0 + } else { + // If we are already at the first item, there is no "previous" voltage to worry about. + // We can use a value lower than any possible voltage to allow the decrease. + -1 // Effectively negative infinity + } + + // 3. Simple Case: If we can decrease the current voltage without it becoming equal to or less than the previous one. + if (currentVoltage - decreament > previousVoltage) { + Timber.d("[EM] Simple decrease for $currentKey") + voltageTable.decreaseVoltageTableValue(currentKey, decreament) + return // We are done + } + + // first key pair + val firstSelectedCalibVoltageIndex = Energies[0] + val firstKey = Pair(pulseWidth, firstSelectedCalibVoltageIndex) + val firstVoltage = voltageTable[firstKey]?.value ?: 0 + + if (firstVoltage - decreament < 0) { + Timber.d("[EM] Cannot decrease voltage for $firstVoltage, already at or below min (0).") + return + } + + // 4. Cascade Case: If decreasing the voltage would conflict with the previous one. + // We must decrease the voltage for the current level and all PREVIOUS levels by 1. + Timber.d("[EM] Cascade decrease starting from index $selectedVoltageIndex down to 0") + for (i in selectedVoltageIndex downTo 0) { + val energyForIndex = Energies[i] + val keyToUpdate = Pair(pulseWidth, energyForIndex) + val voltageToUpdate = voltageTable[keyToUpdate]?.value ?: 0 + + // Ensure we don't decrease any value below the minimum limit + if (voltageToUpdate > 0) { + voltageTable.decreaseVoltageTableValue(keyToUpdate, decreament) + } else { + // If any value in the chain is already at min, we can't pull it further. + // Stop the operation to maintain integrity. + Timber.w("[EM] Cascade stopped at $keyToUpdate, value is already at min voltage (0).") + break + } + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewEngineerView( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ), + + engineerViewModel:EngineerViewModel = EngineerViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current +) +) { + EngineerScreen( + paddingValues = PaddingValues(top = 50.px.dp), + mainViewModel = mainViewModel, + engineerViewModel = engineerViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerTitltBarView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerTitltBarView.kt new file mode 100644 index 0000000..a759ae0 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerTitltBarView.kt @@ -0,0 +1,133 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun EngineerTitltBarView( + title: String = stringResource(R.string.engineer_mode), + height: Dp = 40.px.dp, + onOSSetupClick: () -> Unit= {}, + onCloseClick: () -> Unit= {}, +) { + Row(modifier = Modifier + .fillMaxWidth() + .height(height) + , horizontalArrangement = Arrangement.Center + , verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = RobotoTypography.headlineSmall, + fontWeight = FontWeight.Light, + fontSize = 28.px.sp, + color = Color.Black, + ) + + Spacer(modifier = Modifier.weight(1f)) + + TextButton( + onClick = { + Timber.d("Perform action when button is clicked") + onOSSetupClick() + }, + modifier = Modifier + .size(107.px.dp, 34.px.dp) + .clip(RoundedCornerShape(10.px.dp)) + .border( + width = 1.px.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(10.px.dp) + ) + //.shadow(elevation = 2.px.dp, shape = RoundedCornerShape(12.px.dp)) + //.padding(top = 1.5.px.dp, bottom = 1.5.px.dp, start = 6.px.dp, end = 6.px.dp) + .background(Color(200, 56, 4)) + ) { + Text( + text = stringResource(R.string.os_setup), + style = RobotoTypography.titleSmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.White + ) + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + IconButton( + onClick = onCloseClick, + modifier = Modifier + .size(90.px.dp, 36.px.dp), + colors = IconButtonDefaults.iconButtonColors( + contentColor = Color.Transparent, // Icon and Text color + containerColor = Color.Transparent, // Background color + disabledContentColor = Color.Transparent, // Icon color when disabled + disabledContainerColor = Color.Transparent, // Background color when disabled + ) + ) { + Row { + Image( + painter = painterResource(id = R.drawable.ic_x), + contentDescription = null, + modifier = Modifier.size(18.px.dp), + contentScale = ContentScale.Fit, + ) + Spacer(modifier = Modifier.width(4.px.dp)) + Text( + text = stringResource(R.string.close), + style = RobotoTypography.titleSmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } +} + + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) + +@Composable +fun PreviewEngineerTitltBarView() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + EngineerTitltBarView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerViewModel.kt new file mode 100644 index 0000000..84a4a3b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/EngineerViewModel.kt @@ -0,0 +1,223 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.laseroptek.raman.const.VoltageTable +import com.laseroptek.raman.data.model.LifeTime +import com.laseroptek.raman.data.model.serial.EnergyMeasured +import com.laseroptek.raman.data.model.serial.LaserStatus +import com.laseroptek.raman.data.model.serial.Temperature +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.utils.DispatcherProvider +import com.laseroptek.raman.utils.ext.setVoltageTableValue +import com.laseroptek.raman.utils.ext.toVoltageMutableStateFlowMap +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class EngineerViewModel @Inject constructor( + private val preferenceRepository: PreferenceRepository, + private val serialPortRepository: SerialPortRepository, + private val databaseRepository: DatabaseRepository, + private val dispatcherProvider: DispatcherProvider, + private val applicationContext: Context, +) : ViewModel() { + + + // isVoltageSelectedElseReptition (Engineer) + private val _isVoltageSelectedElseReptition: MutableStateFlow = MutableStateFlow(true) + val isVoltageSelectedElseReptition = _isVoltageSelectedElseReptition.asStateFlow() + fun setIsVoltageSelectedElseReptition(value: Boolean) { + _isVoltageSelectedElseReptition.value = value + } + + // selectedCalibRepetitionIndex (Engineer) + private val _selectedCalibRepetitionIndex: MutableStateFlow = MutableStateFlow(0) + val selectedCalibRepetitionIndex = _selectedCalibRepetitionIndex.asStateFlow() + fun setSelectedCalibRepetitionIndex(value: Int) { + _selectedCalibRepetitionIndex.value = value + } + + // selectedCalibVoltageIndex (Engineer) + private val _selectedCalibVoltageIndex: MutableStateFlow = MutableStateFlow(0) + val selectedCalibVoltageIndex = _selectedCalibVoltageIndex.asStateFlow() + fun setSelectedCalibVoltageIndex(value: Int) { + _selectedCalibVoltageIndex.value = value + } + + // selectedCalibPulseWidthIndex (Engineer) + private val _selectedCalibPulseWidthIndex: MutableStateFlow = MutableStateFlow(0) + val selectedCalibPulseWidthIndex = _selectedCalibPulseWidthIndex.asStateFlow() + fun setSelectedCalibPulseWidthIndex(value: Int) { + _selectedCalibPulseWidthIndex.value = value + } + + // laserStatus (Engineer) + private val _laserStatus: MutableStateFlow = MutableStateFlow(LaserStatus()) + val laserStatus = _laserStatus.asStateFlow() + fun setLaserStatus(laserStatus: LaserStatus) { + _laserStatus.value = laserStatus.copy() + } + + // voltageTable (Engineer) - (PulseWidth(ms), Energy(J)) -> Voltage(V) + private val _voltageTable: MutableStateFlow, MutableStateFlow>> = + VoltageTable.toVoltageMutableStateFlowMap() + val voltageTable = _voltageTable.asStateFlow() + fun setVoltageTable(v: MutableMap, MutableStateFlow>) { + _voltageTable.value = v + v.forEach { + _voltageTable.value.setVoltageTableValue(it.key, it.value.value) + } + } + + // load voltageTable (Engineer) + suspend fun loadVoltageTableFromPreference() { + val v = preferenceRepository.getVoltageTableFromPreference().first() + val voltageTableMap = voltageTable.value + Timber.d("Eng: loadVoltageTableFromPreference() begin") + v.forEach { (key, value) -> + val voltageTableVolFlow = voltageTableMap[key] + if (voltageTableVolFlow != null) { + voltageTableMap.setVoltageTableValue(key, value) + //Timber.d("Eng: loadVoltageTableFromPreference() key: $key, value: $value") + } + } + Timber.d("Eng: loadVoltageTableFromPreference() finish") + } + + // save voltageTable (Engineer) + suspend fun saveVoltageTableToPreference() { + preferenceRepository.saveVoltageTableToPreference( + voltageTable + .value + .mapValues { (_, v) -> v.value } + ) + } + + // temperature (WRITE) + private val _temperature_write: MutableStateFlow = MutableStateFlow(Temperature()) + val temperature_write = _temperature_write.asStateFlow() + fun setTemperatureWrite(t: Temperature) { + _temperature_write.value = t + } + + suspend fun saveTemeratureWriteToPreference() { + preferenceRepository.saveTemperatureWriteToPreference(temperature_write.value) + } + + suspend fun loadTemeratureWriteFromPreference() { + val temperatureWriteValue = preferenceRepository.getTemperatureWriteFromPreference().first() + Timber.d("loadTemeratureWriteFromPreference: temperatureWriteValue: setTemperatureWrite($temperatureWriteValue)") + setTemperatureWrite(temperatureWriteValue) + } + + + // lifeTime - Reserved for future use (Int[8]) + private val _lifeTime: MutableStateFlow = MutableStateFlow(LifeTime()) + val lifeTime = _lifeTime.asStateFlow() + fun setLifeTime(t: LifeTime) { + _lifeTime.value = t + } + + suspend fun saveLifeTimeToPreference() { + preferenceRepository.saveLifeTimeToPreference(lifeTime.value) + } + + suspend fun loadLifeTimeFromPreference() { + val lifeTimeValue = preferenceRepository.getLifeTimeFromPreference().first() + Timber.d("loadLifeTimeFromPreference: lifeTimeValue: setLifeTime($lifeTimeValue)") + setLifeTime(lifeTimeValue) + } + + + // EnergyMeasured Write (Energy Detect - Write value from Engineer Mode) + private val _energyMeasuredWrite: MutableStateFlow = MutableStateFlow(EnergyMeasured()) + val energyMeasuredWrite: MutableStateFlow = _energyMeasuredWrite + fun setEnergyMeasuredWrite(m: EnergyMeasured) { + _energyMeasuredWrite.value = m + } + + suspend fun saveEnergyMeasuredWriteToPreference() { + preferenceRepository.saveEnergyMeasuredWriteToPreference(energyMeasuredWrite.value) + } + suspend fun loadEnergyMeasuredWriteFromPreference() { + val energyMeasuredWrite = preferenceRepository.getEnergyMeasuredWriteFromPreference().first() + Timber.d("loadEnergyMeasuredWriteFromPreference: energyMeasuredWrite: setEnergyMeasuredWrite($energyMeasuredWrite)") + setEnergyMeasuredWrite(energyMeasuredWrite) + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // member functions + + fun factoryDefault() { + Timber.d("factoryDefault begins") + + val voltageTableMap = voltageTable.value + VoltageTable.forEach { (key, value) -> + val voltageTableVolFlow = voltageTableMap[key] + if (voltageTableVolFlow != null) { + voltageTableMap.setVoltageTableValue(key, value) + } + } + + //setQSwitch(QSwitch()) + + setTemperatureWrite(Temperature()) + setLifeTime(LifeTime()) + setEnergyMeasuredWrite(EnergyMeasured()) + + Timber.d("factoryDefault begins") + } + + suspend fun saveToPreference() { + Timber.d("saveToPreference begins") + + saveVoltageTableToPreference() + //saveQSwitchToPreference() + //saveGuideBeamMinToPreference() + //saveGuideBeamMaxToPreference() + saveTemeratureWriteToPreference() + saveLifeTimeToPreference() + saveEnergyMeasuredWriteToPreference() + + Timber.d("saveToPreference begins") + } + + suspend fun loadFromPreference() { + Timber.d("loadFromPreference begins") + + loadVoltageTableFromPreference() + //loadQSwitchFromPreference() + //loadGuideBeamMinFromPreference() + //loadGuideBeamMaxFromPreference() + loadTemeratureWriteFromPreference() + loadLifeTimeFromPreference() + loadEnergyMeasuredWriteFromPreference() + + Timber.d("loadFromPreference begins") + } + + init { + Timber.d("init begins") + + viewModelScope.launch { + loadVoltageTableFromPreference() + //loadQSwitchFromPreference() + //loadGuideBeamMinFromPreference() + //loadGuideBeamMaxFromPreference() + loadTemeratureWriteFromPreference() + loadLifeTimeFromPreference() + loadEnergyMeasuredWriteFromPreference() + } + + Timber.d("init finish") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/GuideBeamView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/GuideBeamView.kt new file mode 100644 index 0000000..727446a --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/GuideBeamView.kt @@ -0,0 +1,362 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.const.MinMaxUpDownState +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GuideBeamView( + modifier: Modifier = Modifier, + onClick: (MinMaxUpDownState) -> Unit = {}, + minValue: Int = 0, + maxValue: Int = 0, + title:String = "" +) { + val scope = rememberCoroutineScope() + val customPressTimeout = 1000L + + // Guide Beam + Row(modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // Guide Beam - title + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + // Guide Beam - min + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1.2f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Min: %4d".format(minValue), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.Start + ) + Spacer(modifier = Modifier.width(5.px.dp)) + + // Min Down (<) + Column( + modifier = Modifier + /* + .noRippleClickable( + onClick = { + onClick.invoke(MinMaxUpDownState.MinDown) + } + ) + */ + .pointerInput(Unit) { // pass `Unit` or any key that, when changed, should reset the gesture detector + awaitEachGesture { + var longPressOccurred = false + + awaitFirstDown() + val job = scope.launch(Dispatchers.Default) { + delay(customPressTimeout) + + longPressOccurred = true + + // Long press detected + while (true) { + Timber.d("Long press detected, starting repeating timer...") + onClick.invoke(MinMaxUpDownState.MinLongDown) + delay(300) // A responsive 300ms interval + } + } + waitForUpOrCancellation() + if (job.isActive) { + job.cancel() + + if (!longPressOccurred) { + // If the job is still active, it means it was a short press + Timber.d("Short press detected.") + onClick.invoke(MinMaxUpDownState.MinDown) + } else { + Timber.d("Up event after long press. Short press ignored.") + } + } + } + } + .size(25.px.dp, 25.px.dp) + .border(width = 1.px.dp, color = Color.Gray, RoundedCornerShape(5.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "<", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + ) + } + Spacer(modifier = Modifier.width(5.px.dp)) + // Min Up (>) + Column( + modifier = Modifier + /* + .noRippleClickable( + onClick = { + onClick.invoke(MinMaxUpDownState.MinUp) + } + ) + */ + .pointerInput(Unit) { // pass `Unit` or any key that, when changed, should reset the gesture detector + awaitEachGesture { + var longPressOccurred = false + + awaitFirstDown() + val job = scope.launch(Dispatchers.Default) { + delay(customPressTimeout) + + // Long press detected + longPressOccurred = true + + while (true) { + Timber.d("MinLongUp") + onClick.invoke(MinMaxUpDownState.MinLongUp) + delay(300) // A responsive 300ms interval + } + } + waitForUpOrCancellation() + if (job.isActive) { + job.cancel() + if (!longPressOccurred) { + // If the job is still active, it means it was a short press + Timber.d("MinUp") + onClick.invoke(MinMaxUpDownState.MinUp) + } else { + Timber.d("Up event after long press. Short press ignored.") + } + + } + } + } + .size(25.px.dp, 25.px.dp) + .border(width = 1.px.dp, color = Color.Gray, RoundedCornerShape(5.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = ">", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + } + } + + // Guide Beam - max + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1.2f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Max: %4d".format(maxValue), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.Start + ) + Spacer(modifier = Modifier.width(5.px.dp)) + // Max Down + Column( + modifier = Modifier + /* + .noRippleClickable( + onClick = { + onClick.invoke(MinMaxUpDownState.MaxDown) + } + ) + */ + .pointerInput(Unit) { // pass `Unit` or any key that, when changed, should reset the gesture detector + awaitEachGesture { + var longPressOccurred = false + + awaitFirstDown() + val job = scope.launch(Dispatchers.Default) { + delay(customPressTimeout) + + longPressOccurred = true + + // Long press detected + while (true) { + Timber.d("MaxLongDown") + onClick.invoke(MinMaxUpDownState.MaxLongDown) + delay(300) // A responsive 300ms interval + } + } + waitForUpOrCancellation() + if (job.isActive) { + job.cancel() + + // If the job is still active, it means it was a short press + if (!longPressOccurred) { + Timber.d("MaxDown") + onClick.invoke(MinMaxUpDownState.MaxDown) + } else { + Timber.d("Up event after long press. Short press ignored.") + } + } + } + } + .size(25.px.dp, 25.px.dp) + .border(width = 1.px.dp, color = Color.Gray, RoundedCornerShape(5.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "<", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + ) + } + Spacer(modifier = Modifier.width(5.px.dp)) + // Max Up + Column( + modifier = Modifier + /* + .noRippleClickable( + onClick = { + onClick.invoke(MinMaxUpDownState.MaxUp) + } + ) + */ + .pointerInput(Unit) { + awaitEachGesture { + var longPressOccurred = false + + awaitFirstDown() + val job = scope.launch(Dispatchers.Default) { + delay(customPressTimeout) + + longPressOccurred = true + + // Long press detected + while (true) { + Timber.d("MaxLongUp") + onClick.invoke(MinMaxUpDownState.MaxLongUp) + delay(300) // A responsive 300ms interval + } + } + waitForUpOrCancellation() + if (job.isActive) { + job.cancel() + + // If the job is still active, it means it was a short press + if (!longPressOccurred) { + Timber.d("MaxUp") + onClick.invoke(MinMaxUpDownState.MaxUp) + } else { + Timber.d("Up event after long press. Short press ignored.") + } + } + } + } + .size(25.px.dp, 25.px.dp) + .border(width = 1.px.dp, color = Color.Gray, RoundedCornerShape(5.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = ">", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + } + } + } + +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewGuideBeamView() { + Row { + GuideBeamView( + modifier = Modifier + .size(640.px.dp, 80.px.dp), + title = "Guide Beam", + onClick = { v -> + Timber.d("Clicked $v") + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/HourItemView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/HourItemView.kt new file mode 100644 index 0000000..7e2d3c1 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/HourItemView.kt @@ -0,0 +1,132 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +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.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun HourItemView( + modifier: Modifier = Modifier, + hour: Int = 0, + onClick: () -> Unit = {}, + title:String = "" +) { + Row(modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1.2f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "%9d".format(hour), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Hour", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + //.noRippleClickable(onClick = onDeviceOpTimeClick) + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.End + ) { + Image( + modifier = Modifier + .size(18.px.dp, 18.px.dp) + .noRippleClickable(onClick = { onClick.invoke() }), + painter = painterResource(id = R.drawable.ic_edit), + contentDescription = null, + colorFilter = Color.Black.let { ColorFilter.tint(it) } + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewHourItemView() { + Row { + HourItemView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + title = "Hour Item View" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/LifeTimeView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/LifeTimeView.kt new file mode 100644 index 0000000..86133b5 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/LifeTimeView.kt @@ -0,0 +1,111 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.const.lifeTimeTypes +import com.laseroptek.raman.data.model.LifeTime +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun LifeTimeView( + lifeTime: LifeTime = LifeTime(), + onClick: (Int) -> Unit = {_ -> }, +) { + Column(modifier = Modifier + //.noRippleClickable(onClick = onClick) + .size(388.px.dp, 276.px.dp) + .clip(RoundedCornerShape(12.px.dp)) + .border(width = 1.px.dp, color = Color(209, 209, 209), shape = RoundedCornerShape(10.px.dp)) + .background(Color.White) + .padding(16.px.dp), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + //Spacer(modifier = Modifier.height(10.px.dp)) + + // Life Time (Title) + SectionTitle( + title = "Life Time", + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + ) + + Spacer(modifier = Modifier.height(10.px.dp)) + + // Temp 0..7 + for (i in 0..lifeTimeTypes.size -1) { + val hour = when (i) { + 0 -> lifeTime.lamp + 1 -> lifeTime.hp5x5 + 2 -> lifeTime.hp7x7 + 3 -> lifeTime.hp10x10 + 4 -> lifeTime.hp12x12 + 5 -> lifeTime.hp3x15 + 6 -> lifeTime.detector + 7 -> lifeTime.water + else -> 0 + } + HourItemView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + title = lifeTimeTypes[i], + hour = hour, + onClick = { + Timber.d("onClick > Temp $i (${lifeTimeTypes[i]})") + onClick.invoke(i) + } + ) + + if (i < lifeTimeTypes.size -1) { + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewLifeTimeView() { + LifeTimeView() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/PulseWidthView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/PulseWidthView.kt new file mode 100644 index 0000000..81044ea --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/PulseWidthView.kt @@ -0,0 +1,120 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.PulseDurations +import com.laseroptek.raman.ui.theme.PretendardTypography +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + + +@Composable +fun PulseWidthView( + onClick: (Int) -> Unit = {}, + selectedCalibPulseWidthIndex: Int = 0, + title:String = stringResource(R.string.pulse_width_title) +) { + Column( + Modifier + .fillMaxWidth() + .height(52.px.dp) + ) { + // Pulse-width title + Row( + Modifier + .fillMaxWidth() + .height(22.px.dp) + .clip(RoundedCornerShape(topStart = 11.px.dp, topEnd = 11.px.dp)) + .background(Color.Black), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = Color.White, + style = PretendardTypography.titleSmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + } + + // Pulse-width value + Row( + modifier = Modifier + .fillMaxWidth() + .height(30.px.dp) + .border(1.dp, color = Color.Gray, shape = RectangleShape), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + for (i in 0..PulseDurations.size -1) { + + val text = if (PulseDurations[i] > 9) { + "${PulseDurations[i].toInt()}" + } else { + "${PulseDurations[i]}" + } + + Column( + Modifier + .noRippleClickable(onClick = { onClick.invoke(i) }) + .fillMaxWidth() + .weight(1f) + .height(31.px.dp) + .border(0.5.dp, color = Color.Gray, shape = RectangleShape) + .background( + if (i == selectedCalibPulseWidthIndex) Color( + 255, + 204, + 0 + ) else Color.White + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = text, + color = Color.Black, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewPulseWidthView() { + Row { + PulseWidthView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/RepetitionView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/RepetitionView.kt new file mode 100644 index 0000000..5842dac --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/RepetitionView.kt @@ -0,0 +1,106 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.Repetitions +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun RepetitionView( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + selectedCalibRepetitionIndex: Int = 0, + isVoltageSelectedElseReptition: Boolean = false, + title:String = "" +) { + Column(modifier = modifier + .border( + width = 1.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(10.px.dp) + ) + ) { + // title + Row( + Modifier + .fillMaxWidth() + .height(22.px.dp) + .clip(RoundedCornerShape(topStart = 10.px.dp, topEnd = 10.px.dp)) + .background(Color(243, 205, 81)), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.repetition), + color = Color.Black, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 12.px.sp, + ) + } + + // value + Row( + Modifier + .fillMaxWidth() + .height(26.px.dp) + .clip(RoundedCornerShape(bottomStart = 10.px.dp, bottomEnd = 10.px.dp)) + .padding(1.px.dp) + .background(if (!isVoltageSelectedElseReptition) Color.Green.copy(alpha = 0.1f) else Color.White) + .noRippleClickable(onClick = { onClick.invoke() }), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Timber.d("%.1f", Repetitions.get(selectedCalibRepetitionIndex)) + Text( + text = "%.1f".format(Repetitions.get(selectedCalibRepetitionIndex)), + color = Color.Black, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewRepetitionView() { + Row { + RepetitionView( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + title = "Temp 1" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/RepetitionVoltageControlView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/RepetitionVoltageControlView.kt new file mode 100644 index 0000000..48e93e3 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/RepetitionVoltageControlView.kt @@ -0,0 +1,352 @@ +package com.laseroptek.raman.ui.screens.engineer + + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber + + +@Composable +fun RepetitionVoltageControlView( + modifier: Modifier = Modifier, + onClickDtoV: () -> Unit = {}, + onClickVtoD: () -> Unit = {}, + onClickUp: () -> Unit = {}, + onClickUpLongPress: () -> Unit = {}, + onClickDown: () -> Unit = {}, + onClickDownLongPress: () -> Unit = {}, +) { + val scope = rememberCoroutineScope() + + //val currentViewConfiguration = LocalViewConfiguration.current + val customPressTimeout = 1000L // Desired long press duration in milliseconds (e.g., 1 second) + + //var longPressed by remember { mutableStateOf(false) } + + // Create a custom ViewConfiguration, remembering it to avoid recreation on recomposition + /* + val customViewConfiguration = remember(currentViewConfiguration) { + object : ViewConfiguration by currentViewConfiguration { // Delegate to the current one + override val longPressTimeoutMillis: Long = customPressTimeout + // You can also override other properties like doubleTapTimeoutMillis if needed + // override val doubleTapTimeoutMillis: Long = customDoubleTapTimeout + } + } + */ + + Column( + modifier = modifier + .background(Color.Transparent), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row { + // left (D->V, D<-V) + Column(modifier = Modifier + .fillMaxSize() + .weight(1f), + verticalArrangement = Arrangement.Bottom, + ) { + //Spacer(modifier = Modifier.weight(1f)) + + // D->V + Row( + Modifier + .noRippleClickable(onClick = onClickDtoV) + .fillMaxWidth() + .height(32.px.dp) + .clip(RoundedCornerShape(16.px.dp)) + .background(Color.Black), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "D ", + color = Color.White, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + Image( + painter = painterResource(id = R.drawable.ic_arrow_right), + contentDescription = null, + modifier = Modifier.size(18.px.dp), + contentScale = ContentScale.FillBounds, + colorFilter = ColorFilter.tint(Color.White) + ) + Text( + text = " V", + color = Color.White, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + } + Spacer(modifier = Modifier.height(8.px.dp)) + + // D<-V + Row( + Modifier + .noRippleClickable(onClick = onClickVtoD) + .fillMaxWidth() + .height(32.px.dp) + .clip(RoundedCornerShape(16.px.dp)) + .background(Color.Black), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "D ", + color = Color.White, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + Image( + painter = painterResource(id = R.drawable.ic_arrow_left), + contentDescription = null, + modifier = Modifier.size(18.px.dp), + contentScale = ContentScale.FillBounds, + colorFilter = ColorFilter.tint(Color.White) + ) + Text( + text = " V", + color = Color.White, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + } + + } + + Spacer(modifier = Modifier.width(4.px.dp)) + + // right (Up, Down) + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + verticalArrangement = Arrangement.Bottom, + ) { + //Spacer(modifier = Modifier.weight(1f)) + + // Up + //CompositionLocalProvider(LocalViewConfiguration provides customViewConfiguration) { + Row( + Modifier + //.noRippleClickable(onClick = onClickUp) + .pointerInput(Unit) { // pass `Unit` or any key that, when changed, should reset the gesture detector + awaitEachGesture { + var longPressOccurred = false + + awaitFirstDown() + val job = scope.launch(Dispatchers.Default) { + delay(customPressTimeout) + + longPressOccurred = true + + // Long press detected + while (true) { + Timber.d("Long press detected, starting repeating timer...") + onClickUpLongPress.invoke() + delay(300) // A responsive 300ms interval + } + } + waitForUpOrCancellation() + if (job.isActive) { + job.cancel() + + // If the job is still active, it means it was a short press + if (!longPressOccurred) { + Timber.d("Short press detected.") + onClickUp.invoke() + } else { + Timber.d("Up event after long press. Short press ignored.") + } + } + } + } + .fillMaxWidth() + .height(32.px.dp) + .border( + 2.dp, + color = Color(70, 70, 70), + shape = RoundedCornerShape(10.px.dp) + ) + .clip(RoundedCornerShape(10.px.dp)) + .background( + brush = Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color(250, 250, 250), + 0.5f to Color(198, 198, 198), + 1.0f to Color(250, 250, 250), + ) + ) + ) + .dropShadow( + shape = RoundedCornerShape(0.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.14f), + blur = 7.px.dp, + offsetY = 5.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(0.px.dp), + color = Color(255, 255, 255), + blur = 1.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_up), + contentDescription = null, + modifier = Modifier.size(30.px.dp, 27.px.dp), + colorFilter = Color.Black.let { ColorFilter.tint(it) } + ) + } + //} + + + Spacer(modifier = Modifier.height(8.px.dp)) + + // Down + //CompositionLocalProvider(LocalViewConfiguration provides customViewConfiguration) { + Row( + Modifier + .pointerInput(Unit) { + awaitEachGesture { + var longPressOccurred = false + + awaitFirstDown() + val job = scope.launch(Dispatchers.Default) { + delay(customPressTimeout) + // Long press detected + longPressOccurred = true + + while (true) { + Timber.d("Long press detected, starting repeating timer...") + onClickDownLongPress.invoke() + delay(300) // A responsive 300ms interval + } + } + waitForUpOrCancellation() + if (job.isActive) { + job.cancel() + // If the job is still active, it means it was a short press + Timber.d("Short press detected.") + if (!longPressOccurred) { + Timber.d("Short press detected.") + onClickDown.invoke() + } else { + Timber.d("Up event after long press. Short press ignored.") + } + } + } + } + .fillMaxWidth() + .height(32.px.dp) + .border( + 2.dp, + color = Color(70, 70, 70), + shape = RoundedCornerShape(10.px.dp) + ) + .clip(RoundedCornerShape(10.px.dp)) + .background( + brush = Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color(250, 250, 250), + 0.5f to Color(198, 198, 198), + 1.0f to Color(250, 250, 250), + ) + ) + ) + .dropShadow( + shape = RoundedCornerShape(0.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.14f), + blur = 7.px.dp, + offsetY = 5.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(0.px.dp), + color = Color(255, 255, 255), + blur = 1.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_down), + contentDescription = null, + modifier = Modifier.size(30.px.dp, 27.px.dp), + colorFilter = Color.Black.let { ColorFilter.tint(it) } + ) + } + //} + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=240dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewRepetitionVoltageControlView() { + Row { + RepetitionVoltageControlView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/SectionTitle.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/SectionTitle.kt new file mode 100644 index 0000000..e147693 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/SectionTitle.kt @@ -0,0 +1,67 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px + +@Composable +fun SectionTitle( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + title:String = "" +) { + Row(modifier = modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Bold, + fontSize = 14.px.sp, + color = Color.Black, + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSectionTitle() { + Row { + SectionTitle( + modifier = Modifier + .fillMaxSize() + .weight(1f), + title = "Life Time" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/SetButton.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/SetButton.kt new file mode 100644 index 0000000..f95a3a7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/SetButton.kt @@ -0,0 +1,55 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px + +@Composable +fun SetButton( + onClick: () -> Unit = {}, + title: String = stringResource(id = R.string.stand_by_title), +) { + TextButton( + onClick = onClick, + modifier = Modifier + .size(388.px.dp, 50.px.dp) + .clip(RoundedCornerShape(10.px.dp)) + .background(Color(243, 205, 81)) + ) { + Text( + text = title, + style = RobotoTypography.bodyMedium.copy( + color = Color.Black, + fontWeight = FontWeight.Normal, + ), + fontSize = 24.px.sp + ) + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSetButton() { + SetButton( + title = stringResource(id = R.string.set) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/StandByButton.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/StandByButton.kt new file mode 100644 index 0000000..22dfd8c --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/StandByButton.kt @@ -0,0 +1,76 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px + +@Composable +fun StandByButton( + onClick: () -> Unit = {}, + title: String = stringResource(id = R.string.stand_by_title), +) { + // StandBy Button + TextButton( + onClick = onClick, + modifier = Modifier + .size(388.px.dp, 70.px.dp) + .clip(RoundedCornerShape(10.px.dp)) + .background(Color(35, 35, 35)) + .border( + width = 0.5.px.dp, + color = Color(222, 216, 185) + ) + .dropShadow( + shape = RoundedCornerShape(0.px.dp), + color = Color(0, 0, 0), + blur = 0.px.dp, + offsetX = 0.px.dp, + offsetY = 1.66.px.dp, + spread = 0.px.dp + ) + ) { + Text( + text = title, //stringResource(R.string.standby), + style = RobotoTypography.bodyMedium.copy( + brush = Brush.linearGradient( + colors = listOf(Color(255,238,204), Color(222,188,76)), + start = Offset(0f, 50f), + end = Offset(100f, 50f) + ), + fontWeight = FontWeight.Normal, + ), + fontSize = 36.px.sp + ) + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewStandByButton() { + StandByButton( + title = stringResource(id = R.string.stand_by_title) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/TemperatureView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/TemperatureView.kt new file mode 100644 index 0000000..e6f6ca8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/TemperatureView.kt @@ -0,0 +1,119 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.const.temperatureTypes +import com.laseroptek.raman.data.model.serial.Temperature +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun TemperatureView( + temperatureRead: Temperature = Temperature(), + temperatureWrite: Temperature = Temperature(), + onClick: (Int) -> Unit = {_ -> }, +) { + Column(modifier = Modifier + //.noRippleClickable(onClick = onClick) + .size(388.px.dp, 276.px.dp) + .clip(RoundedCornerShape(12.px.dp)) + .border(width = 1.px.dp, color = Color(209, 209, 209), shape = RoundedCornerShape(10.px.dp)) + .background(Color.White) + .padding(16.px.dp), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + //Spacer(modifier = Modifier.height(10.px.dp)) + + // Temp (Title) + SectionTitle( + title = "Temp", + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + ) + + Spacer(modifier = Modifier.height(10.px.dp)) + + // Tempearture + for (i in 0..temperatureTypes.size -1) { + val count1Value = when (i) { + 0 -> temperatureRead.ktp + 1 -> temperatureRead.chamber1 + 2 -> temperatureRead.chamber2 + 3 -> temperatureRead.basePlate + 4 -> temperatureRead.water + else -> 0.0f + } + val count2Value = when (i) { + 0 -> temperatureWrite.ktp + 1 -> temperatureWrite.chamber1 + 2 -> temperatureWrite.chamber2 + 3 -> temperatureWrite.basePlate + 4 -> temperatureWrite.water + else -> 0.0f + } + TwoCountItemView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding( + start = 20.px.dp, + end = 20.px.dp, + //bottom = 10.px.dp + ), + index = i, + count1 = count1Value, + count2 = count2Value, + title = temperatureTypes[i], + onClick = { idx -> + Timber.d("onClick > Temp ${temperatureTypes[i]}") + onClick(idx) + } + ) + + if (i < temperatureTypes.size -1) { + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSerialNumber() { + TemperatureView() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/TwoCountItemView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/TwoCountItemView.kt new file mode 100644 index 0000000..612009c --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/TwoCountItemView.kt @@ -0,0 +1,172 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +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.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun TwoCountItemView( + modifier: Modifier = Modifier, + index: Int = 0, + count1: Float = 0.0f, + count2: Float = 0.0f, + onClick: (Int) -> Unit = {}, + title:String = "" +) { + Row(modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1.5f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "%.1f".format(count1), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Read", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "%.1f".format(count2), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Write", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + Column( + modifier = Modifier + //.noRippleClickable(onClick = onDeviceOpTimeClick) + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.End + ) { + Image( + modifier = Modifier + .size(18.px.dp, 18.px.dp) + .noRippleClickable(onClick = { onClick.invoke(index) }), + painter = painterResource(id = R.drawable.ic_edit), + contentDescription = null, + colorFilter = Color.Black.let { ColorFilter.tint(it) } + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewTwoCountItemView() { + Row { + TwoCountItemView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + title = "Chamber2" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/VersionView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/VersionView.kt new file mode 100644 index 0000000..5a4d715 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/VersionView.kt @@ -0,0 +1,344 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +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.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.model.serial.Version +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.formatWithDots +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.ext.toFullStringRepresentation +import timber.log.Timber + +@Composable +fun VersionView( + title: String = stringResource(id = R.string.program_version), + version: Version = Version(), + productSerialList: List = listOf(), + laserHandSerialList: List = listOf(), + powerSupplySerialList: List = listOf(), + onClick: (Int) -> Unit = {}, +) { + var index by remember { mutableStateOf(0) } + + val selectedBackgroundBrush = Brush.verticalGradient( + colors = listOf( + Color(47, 51, 57), + Color(83, 88, 97), + ) + ) + + val selectedBorderBrush = Brush.verticalGradient( + colors = listOf( + Color(92, 99, 107), + Color(34, 34, 1), + ) + ) + + Timber.d("version: ${version}") + + Column(modifier = Modifier + .noRippleClickable(onClick = { onClick.invoke( index) }) + .size(388.px.dp, 154.px.dp) + .clip(RoundedCornerShape(12.px.dp)) + .border(width = 1.px.dp, color = Color(209, 209, 209), shape = RoundedCornerShape(10.px.dp)) + .background(Color.White) + .padding(16.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Title + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Row( + modifier = Modifier + .size(296.px.dp, 38.px.dp) + .border( + width = 0.5.px.dp, + color = Color(209,209,209), + shape = RoundedCornerShape(8.px.dp) + ) + ) { + // Serial Number + Column( + modifier = Modifier + .noRippleClickable(onClick = { index = 0 }) + .size(145.px.dp, 32.px.dp) + .background( + brush = if (index == 0) selectedBackgroundBrush else SolidColor(Color.Transparent), + shape = RoundedCornerShape(8.px.dp) + ) + .border( + width = 0.5.px.dp, + brush = if (index == 0) selectedBorderBrush else SolidColor(Color.Transparent), + shape = RoundedCornerShape(8.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (index == 0) Color(243, 205, 81).copy(alpha = 0.2f) else Color.Transparent, + blur = 3.5.px.dp, + offsetY = 0.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (index == 0) Color(0, 0, 0).copy(alpha = 0.1f) else Color.Transparent, + blur = 1.75.px.dp, + offsetY = 1.75.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Serial Number", + style = RobotoTypography.bodySmall.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.ExtraLight + ), + textAlign = TextAlign.Center, + color = if (index == 0) Color.White else Color.Black, + ) + } + + Spacer(modifier = Modifier.width(6.px.dp)) + + // Program Version + Column( + modifier = Modifier + .noRippleClickable(onClick = { index = 1 }) + .size(145.px.dp, 32.px.dp) + .background( + brush = if (index == 1) selectedBackgroundBrush else SolidColor(Color.Transparent), + shape = RoundedCornerShape(8.px.dp) + ) + .border( + width = 0.5.px.dp, + brush = if (index == 1) selectedBorderBrush else SolidColor(Color.Transparent), + shape = RoundedCornerShape(8.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (index == 1) Color(243, 205, 81).copy(alpha = 0.2f) else Color.Transparent, + blur = 3.5.px.dp, + offsetY = 0.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (index == 1) Color(0, 0, 0).copy(alpha = 0.1f) else Color.Transparent, + blur = 1.75.px.dp, + offsetY = 1.75.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Program Version", + style = RobotoTypography.bodySmall.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.ExtraLight + ), + textAlign = TextAlign.Center, + color = if (index == 1) Color.White else Color.Black, + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Image( + painter = painterResource(id = R.drawable.ic_edit), + contentDescription = null, + modifier = Modifier.size(18.px.dp, 18.px.dp), + colorFilter = Color.Black.let { ColorFilter.tint(it) } + ) + } + } + + Spacer(modifier = Modifier.height(10.px.dp)) + + // Product + Row( + modifier = Modifier + .fillMaxWidth() + .height(30.px.dp), + ) { + Column( + modifier = Modifier + .size(100.px.dp, 30.px.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "Product", + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Column( + modifier = Modifier + .size(248.px.dp, 30.px.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = + if (index == 0) productSerialList.toFullStringRepresentation() + else "Code: %s, Name: %s".format(version.verCode.formatWithDots(), version.verName), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + HorizontalDivider( + modifier = Modifier + .padding(start = 10.px.dp, end=10.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Laser Hand + Row( + modifier = Modifier + .fillMaxWidth() + .height(30.px.dp), + ) { + Column( + modifier = Modifier + .size(100.px.dp, 30.px.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "Laser Hand", + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Column( + modifier = Modifier + .size(248.px.dp, 30.px.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = + if (index == 0) laserHandSerialList.toFullStringRepresentation() + else "H/W: %s, S/W: %s".format(version.lhHwRev.formatWithDots(), version.lhSwVer.formatWithDots()), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + HorizontalDivider( + modifier = Modifier + .padding(start = 10.px.dp, end=10.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Power Supply + Row( + modifier = Modifier + .fillMaxWidth() + .height(30.px.dp), + ) { + Column( + modifier = Modifier + .size(100.px.dp, 30.px.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "Power Supply", + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Column( + modifier = Modifier + .size(248.px.dp, 30.px.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = + if (index == 0) powerSupplySerialList.toFullStringRepresentation() + else "H/W: %s, S/W: %s".format(version.psHwVer, version.psSwVer), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewProgramVersion() { + VersionView() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/VoltageView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/VoltageView.kt new file mode 100644 index 0000000..7df1f28 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/VoltageView.kt @@ -0,0 +1,98 @@ +package com.laseroptek.raman.ui.screens.engineer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.data.model.serial.LaserStatus +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px + +@Composable +fun VoltageView( + modifier: Modifier = Modifier, + laserStatus: LaserStatus = LaserStatus(), + onClick: () -> Unit = {}, + title:String = "" +) { + Column( + modifier = modifier + .border( + width = 1.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(10.px.dp) + ) + ) { + // title + Row( + Modifier + .fillMaxWidth() + .height(22.px.dp) + .clip(RoundedCornerShape(topStart = 10.px.dp, topEnd = 10.px.dp)) + .background(Color(243, 205, 81)), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = Color.Black, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 12.px.sp, + ) + } + + // D (V value) + Row( + Modifier + .fillMaxWidth() + .height(26.px.dp) + .clip(RoundedCornerShape(bottomStart = 10.px.dp, bottomEnd = 10.px.dp)) + .background(Color(255, 255, 255)), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = laserStatus.voltage.toString(), + color = Color.Black, // Color(255, 204, 0), + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewVoltageView() { + Row { + VoltageView( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + title = "Temp 1" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/count/CountPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/count/CountPopup.kt new file mode 100644 index 0000000..0b733e0 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/count/CountPopup.kt @@ -0,0 +1,292 @@ +package com.laseroptek.raman.ui.screens.engineer.count + +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CountPopup( + title: String = stringResource(R.string.lamp_total_count_title), + count: Int = 0, + maxValue: Int = 1000000, // < 백만 (999,999 까지) 7 자리수 (figures) + onClick: (Boolean, Int) -> Unit = { _, _ -> }, +) { + val context = LocalContext.current + var textFieldValue by remember { + mutableStateOf( + // 2. Initialize TextFieldValue with the text and set the cursor to the end. + TextFieldValue( + text = count.toString(), + selection = TextRange(count.toString().length) // This places the cursor at the end + ) + ) + } + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + val performOkClick = { + val text = textFieldValue.text + if (text.isEmpty()) { + Toast.makeText(context, "Input Number", Toast.LENGTH_SHORT).show() + } else { + focusManager.clearFocus() + // Use toIntOrNull for safety, though validation should prevent crashes + val newCount = text.toIntOrNull() ?: 0 + Timber.d("OK action performed with count: ${newCount}") + onClick(true, newCount) + } + } + + FullScreen( + contentAlignment= Alignment.Center + ) { + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 100.px.dp), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .size(312.px.dp, 216.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(start = 24.px.dp, end = 24.px.dp, top = 10.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(62.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 24.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Text + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .clip(RoundedCornerShape(4.px.dp)) + .border( + width = 1.px.dp, + Color.Black.copy(alpha = 0.38f), + shape = RoundedCornerShape(4.px.dp) + ) + .background(Color(245, 245, 245)) + //.padding(10.px.dp) + ) { + OutlinedTextField( + value = textFieldValue, + onValueChange = { newTextFieldValue -> + val newText = newTextFieldValue.text + if (newText.isEmpty()) { + textFieldValue = newTextFieldValue + } else if (newText.all { it.isDigit() }) { + // Safely parse to Long to avoid NumberFormatException on large numbers + val number = newText.toLongOrNull() + if (number != null && number <= maxValue) { + textFieldValue = newTextFieldValue + } else { + // The number is too large or invalid, show a toast + Toast.makeText( + context, + "Input Number exceeded max value of $maxValue", + Toast.LENGTH_SHORT + ).show() + } + } + }, + //label = { Text("Count") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, // KeyboardType.Number, //KeyboardType.Text + imeAction = ImeAction.Done, + //autoCorrect = false + ), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + performOkClick() + }), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + } + + // Divider + HorizontalDivider( + modifier = Modifier, + thickness = 0.5.px.dp, + color = Color.LightGray + ) + + Spacer(Modifier.weight(1f)) + + // Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onLoadClick") + onClick(false, 0) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + performOkClick() + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } + + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewCountPopup( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + CountPopup( + //mainViewModel = mainViewModel + title = stringResource(R.string.lamp_total_count_title), + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/count/FloatPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/count/FloatPopup.kt new file mode 100644 index 0000000..1b21f5d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/count/FloatPopup.kt @@ -0,0 +1,300 @@ +package com.laseroptek.raman.ui.screens.engineer.count + +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.MAX_TEMP_DECIMAL_DIGIT +import com.laseroptek.raman.const.MAX_TEMP_INT_DIGIT +import com.laseroptek.raman.const.MAX_TEMP_VALUE +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FloatPopup( + title: String = stringResource(R.string.lamp_total_count_title), + count: Float = 0.0f, + maxIntegerDigits: Int = MAX_TEMP_INT_DIGIT, // e.g., for 100.0, this would be 3 + maxDecimalDigits: Int = MAX_TEMP_DECIMAL_DIGIT, // e.g., for one decimal place, this is 1 + maxValue: Float = MAX_TEMP_VALUE, + onClick: (Boolean, Float) -> Unit = { _, _ -> }, +) { + val context = LocalContext.current + var textFieldValue by remember { + mutableStateOf( + // 2. Initialize TextFieldValue with the text and set the cursor to the end. + TextFieldValue( + text = count.toString(), + selection = TextRange(count.toString().length) // This places the cursor at the end + ) + ) + } + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + + val performOkClick = remember@{ + val text = textFieldValue.text + + if (text.isEmpty() || text == "." || text.endsWith(".")) { // Check for incomplete input + Toast.makeText(context, "Invalid Number", Toast.LENGTH_SHORT).show() + return@remember + } + + val newFloat = text.toFloat() + if (newFloat > maxValue) { + Toast.makeText(context, "Input Number exceeded max value ${maxValue}", Toast.LENGTH_SHORT).show() + return@remember + } + + focusManager.clearFocus() + Timber.d("OK action performed with count: ${newFloat}") + onClick(true, newFloat) + } + + FullScreen( + contentAlignment= Alignment.Center + ) { + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 100.px.dp), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .size(312.px.dp, 216.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(start = 24.px.dp, end = 24.px.dp, top = 10.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(62.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 24.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Text + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .clip(RoundedCornerShape(4.px.dp)) + .border( + width = 1.px.dp, + Color.Black.copy(alpha = 0.38f), + shape = RoundedCornerShape(4.px.dp) + ) + .background(Color(245, 245, 245)) + //.padding(10.px.dp) + ) { + OutlinedTextField( + value = textFieldValue, + onValueChange = { newTextFieldValue -> + val newText = newTextFieldValue.text + + val regex = """^\d{0,$maxIntegerDigits}(\.\d{0,$maxDecimalDigits})?$""".toRegex() + + if (newText.isEmpty() || regex.matches(newText)) { + textFieldValue = newTextFieldValue + } else { + Toast.makeText( + context, + "Invalid float format or length exceeded.", + Toast.LENGTH_SHORT + ).show() + } + }, + //label = { Text("Float") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, // Use Number for decimals + imeAction = ImeAction.Done, + ), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + performOkClick() + }), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + } + + // Divider + HorizontalDivider( + modifier = Modifier, + thickness = 0.5.px.dp, + color = Color.LightGray + ) + + Spacer(Modifier.weight(1f)) + + // Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onLoadClick") + onClick(false, 0.0f) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + performOkClick() + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } + + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewFloatPopup( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + FloatPopup( + //mainViewModel = mainViewModel + title = stringResource(R.string.lamp_total_count_title), + count = 123.4f + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/dcd/DcdConfigPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/dcd/DcdConfigPopup.kt new file mode 100644 index 0000000..e7648b3 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/dcd/DcdConfigPopup.kt @@ -0,0 +1,290 @@ +package com.laseroptek.raman.ui.screens.engineer.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.const.dcdCanTypeFormatedLists +import com.laseroptek.raman.const.dcdCanTypeLists +import com.laseroptek.raman.const.dcdLifeSpanAdjustLists +import com.laseroptek.raman.const.dcdLifeSpanAdjustStringLists +import com.laseroptek.raman.data.model.DcdType +import com.laseroptek.raman.ui.common.core.WheelPickerDefaults +import com.laseroptek.raman.ui.common.core.WheelTextPicker +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DcdConfigPopup( + mainViewModel: MainViewModel = hiltViewModel(), + //showDcdConfigPopup: MutableState = mutableStateOf(false), + title: String = stringResource(R.string.dcd_title), + //dcdType: MutableStateFlow = MutableStateFlow(DcdType()), + onClick: (Boolean) -> Unit = {}, +) { + val dcdType by mainViewModel.dcdType.collectAsState() + + FullScreen(contentAlignment= Alignment.Center) { + Column( + modifier = Modifier + .size(384.px.dp, 336.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(5.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.px.dp) + .padding(start = 24.px.dp, end = 24.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 24.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Sub Title (108 + x (=36) + 192) = 336 + Row( + modifier = Modifier + .fillMaxWidth() + .height(176.px.dp) + .padding(start = 24.px.dp, end = 24.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ){ + // Can Type + Column( + modifier = Modifier + .size(108.px.dp, 176.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .padding(5.px.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + // SubTitle - CAN Type + Text( + text = stringResource(R.string.can_type_title), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color(73,69,79), + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.weight(1f)) + + // Wheel Picker + WheelTextPicker( + modifier = Modifier.size(108.px.dp, 148.px.dp), + startIndex = dcdCanTypeLists.indexOf(dcdType.canType).takeIf {it != -1} ?: 0, + size = DpSize(108.px.dp, 148.px.dp), + texts = dcdCanTypeFormatedLists, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + if (snappedIndex > 0 && snappedIndex <= dcdCanTypeLists.size) { + val newDcdType = DcdType( + canType = dcdCanTypeLists[snappedIndex-1], + lifeSpan = dcdType.lifeSpan + ) + mainViewModel.setDcdType(newDcdType.copy()) + Timber.d("dcdType.value.canType : $dcdType.value.canType ") + } + return@WheelTextPicker snappedIndex + } + ) + } + + Spacer(Modifier.weight(1f)) + + // Divider + VerticalDivider( + modifier = Modifier + .padding(top = 18.px.dp, bottom =18.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + Spacer(Modifier.weight(1f)) + + // DCD Lifespan Adjust + Column( + modifier = Modifier + .size(192.px.dp, 176.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .padding(5.px.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + // SubTitle - Lifespan Adjust + Text( + text = stringResource(R.string.dcd_liftspan_adjust_title), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color(73,69,79), + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.weight(1f)) + + // Wheel Picker - DCD_Lifespan_Adjust_Layer + WheelTextPicker( + modifier = Modifier.size(108.px.dp, 148.px.dp), + startIndex = dcdLifeSpanAdjustLists.indexOf(dcdType.lifeSpan).takeIf { it != -1 } ?: 0, + size = DpSize(108.px.dp, 148.px.dp), + texts = dcdLifeSpanAdjustStringLists, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + if (snappedIndex > 0 && snappedIndex <= dcdLifeSpanAdjustLists.size) { + Timber.d("snappedIndex: $snappedIndex") + val newDcdType = DcdType( + canType = dcdType.canType, + lifeSpan = dcdLifeSpanAdjustLists[snappedIndex-1] + ) + mainViewModel.setDcdType(newDcdType.copy()) + } + + return@WheelTextPicker snappedIndex + } + ) + } + } + + Spacer(Modifier.weight(1f)) + + // Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onLoadClick") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) + +@Composable +fun PreviewDCDConfigView() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + DcdConfigPopup() + } +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/default/DefaultConfirmPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/default/DefaultConfirmPopup.kt new file mode 100644 index 0000000..4955089 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/default/DefaultConfirmPopup.kt @@ -0,0 +1,143 @@ +package com.laseroptek.raman.ui.screens.engineer.default + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun DefaultConfirmPopup( + title: String = stringResource(R.string.default_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(start = 30.px.dp, end = 30.px.dp, bottom = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewDefaultConfirmPopup() { + remember { mutableStateOf(true) } + DefaultConfirmPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/HandPieceView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/HandPieceView.kt new file mode 100644 index 0000000..1b3385d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/HandPieceView.kt @@ -0,0 +1,132 @@ +package com.laseroptek.raman.ui.screens.engineer.energy + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.validHandPieceTypes +import com.laseroptek.raman.ui.theme.PretendardTypography +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px + + +@Composable +fun HandPieceView( + //onClick: (Int) -> Unit = {}, + hpType: Int = 1, // handpiece type no (1..n-1) + title:String = stringResource(R.string.h_p_title) +) { + Column( + Modifier + .fillMaxWidth() + .height(52.px.dp) + ) { + // HandPiece title + Row( + Modifier + .fillMaxWidth() + .height(22.px.dp) + .clip(RoundedCornerShape(topStart = 11.px.dp, topEnd = 11.px.dp)) + .background(Color.Black), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = Color.White, + style = PretendardTypography.titleSmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 12.px.sp, + textAlign = TextAlign.Center, + lineHeight = 22.px.sp + ) + } + + // HandPiece value + Row( + modifier = Modifier + .fillMaxWidth() + .height(30.px.dp) + .border(1.dp, color = Color.Gray, shape = RectangleShape), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + for (i in 1 .. validHandPieceTypes.size) { + Column( + Modifier + //.noRippleClickable(onClick = { onClick.invoke(i) }) + .fillMaxWidth() + .weight(1f) + .height(31.px.dp) + .border(0.5.dp, color = Color.Gray, shape = RectangleShape) + .background( + if (i == hpType) Color( + 255, + 204, + 0 + ) else Color.White + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = validHandPieceTypes[i - 1], + color = Color.Black, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + textAlign = TextAlign.Center, + lineHeight = 31.px.sp + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + //device = "spec:width=640dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewHandPieceView() { + Row { + HandPieceView( + hpType = 0 // 1..n-1 + ) + Spacer(modifier = Modifier.height(10.px.dp)) + HandPieceView( + hpType = 1 + ) + Spacer(modifier = Modifier.height(10.px.dp)) + HandPieceView( + hpType = 3 + ) + Spacer(modifier = Modifier.height(10.px.dp)) + HandPieceView( + hpType = 5 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/Refer1Popup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/Refer1Popup.kt new file mode 100644 index 0000000..0014d55 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/Refer1Popup.kt @@ -0,0 +1,682 @@ +package com.laseroptek.raman.ui.screens.engineer.energy + +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.DEFAULT_REFER1_VALUE +import com.laseroptek.raman.const.MAX_REFER1_VALUE +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Refer1Popup( + title: String = stringResource(R.string.energy_detect_refer1_title), + hpType: Int = 1, // handpiece type no (1..n-1) + measured: Int = DEFAULT_REFER1_VALUE, // energy measured value (READ) + measuredWrite: Int = DEFAULT_REFER1_VALUE, // energy measured value (WRITE)(INTERNAL) + maxValue: Int = MAX_REFER1_VALUE, + onClickRead: () -> Unit = {}, + onClickCancel: () -> Unit = {}, + onClickSet: (Int) -> Unit = {}, // energy measured value (WRITE)(EXTENRAL) +) { + val context = LocalContext.current + var textFieldValue by remember { + mutableStateOf( + // Initialize TextFieldValue with the text and set the cursor to the end. + TextFieldValue( + text = measuredWrite.toString(), + selection = TextRange(measuredWrite.toString().length) // This places the cursor at the end + ) + ) + } + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + val performSetClick = { + val text = textFieldValue.text + if (text.isEmpty()) { + Toast.makeText(context, "Input Number", Toast.LENGTH_SHORT).show() + } else { + focusManager.clearFocus() + // Use toIntOrNull for safety, though validation should prevent crashes + val newValue = text.toIntOrNull() ?: 0 + Timber.d("OK action performed with value: ${newValue}") + onClickSet(newValue) + } + } + + // TabView + var tabIndex by remember { mutableStateOf(0) } + val selectedBackgroundBrush = Brush.verticalGradient( + colors = listOf( + Color(47, 51, 57), + Color(83, 88, 97), + ) + ) + val selectedBorderBrush = Brush.verticalGradient( + colors = listOf( + Color(92, 99, 107), + Color(34, 34, 1), + ) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + FullScreen { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 50.px.dp), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .size(420.px.dp, 360.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(start = 24.px.dp, end = 24.px.dp, top = 10.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 24.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + Spacer(Modifier.height(10.px.dp)) + + // HandPiece View + Row(modifier = Modifier + .fillMaxWidth() + .height(52.px.dp) + ) { + HandPieceView(hpType = hpType) + } + + Spacer(Modifier.height(10.px.dp)) + + // Tab Selection + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.px.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Row( + modifier = Modifier + .size(296.px.dp, 38.px.dp) + .border( + width = 0.5.px.dp, + color = Color(209,209,209), + shape = RoundedCornerShape(8.px.dp) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Internal + Column( + modifier = Modifier + .noRippleClickable(onClick = { tabIndex = 0 }) + .size(145.px.dp, 32.px.dp) + .background( + brush = if (tabIndex == 0) selectedBackgroundBrush else SolidColor(Color.Transparent), + shape = RoundedCornerShape(8.px.dp) + ) + .border( + width = 0.5.px.dp, + brush = if (tabIndex == 0) selectedBorderBrush else SolidColor( + Color.Transparent + ), + shape = RoundedCornerShape(8.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (tabIndex == 0) Color(243, 205, 81).copy(alpha = 0.2f) else Color.Transparent, + blur = 3.5.px.dp, + offsetY = 0.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (tabIndex == 0) Color(0, 0, 0).copy(alpha = 0.1f) else Color.Transparent, + blur = 1.75.px.dp, + offsetY = 1.75.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Internal", + style = RobotoTypography.bodySmall.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.ExtraLight + ), + textAlign = TextAlign.Center, + color = if (tabIndex == 0) Color.White else Color.Black, + ) + } + + Spacer(modifier = Modifier.width(6.px.dp)) + + // External + Column( + modifier = Modifier + .noRippleClickable(onClick = { tabIndex = 1 }) + .size(145.px.dp, 32.px.dp) + .background( + brush = if (tabIndex == 1) selectedBackgroundBrush else SolidColor(Color.Transparent), + shape = RoundedCornerShape(8.px.dp) + ) + .border( + width = 0.5.px.dp, + brush = if (tabIndex == 1) selectedBorderBrush else SolidColor(Color.Transparent), + shape = RoundedCornerShape(8.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (tabIndex == 1) Color(243, 205, 81).copy(alpha = 0.2f) else Color.Transparent, + blur = 3.5.px.dp, + offsetY = 0.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (tabIndex == 1) Color(0, 0, 0).copy(alpha = 0.1f) else Color.Transparent, + blur = 1.75.px.dp, + offsetY = 1.75.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "External", + style = RobotoTypography.bodySmall.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.ExtraLight + ), + textAlign = TextAlign.Center, + color = if (tabIndex == 1) Color.White else Color.Black, + ) + } + } + } + + // Internal + if (tabIndex == 0) { + Row( + modifier = Modifier + .fillMaxWidth() + .height( 100.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Read", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .background(Color(245,245,245)) + .border(width = 1.px.dp, Color.LightGray,), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = " %d".format(measured), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 16.px.sp, + color = Color.Black.copy(alpha = 0.5f) + ) + } + + HorizontalDivider(thickness = 1.px.dp, color = Color(0,0,0).copy(alpha = 0.38f)) + } + + // Dividier + //Spacer(Modifier.width(10.px.dp)) + VerticalDivider( + color = Color(209, 209, 209), + thickness = 1.px.dp, + modifier = Modifier + .padding(top = 20.px.dp, bottom = 10.px.dp, start = 10.px.dp, end = 10.px.dp) + ) + //Spacer(Modifier.width(10.px.dp)) + + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Write", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .background(Color(245,245,245)) + .border(width = 1.px.dp, Color.LightGray,), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = textFieldValue, + onValueChange = { newTextFieldValue -> + val newText = newTextFieldValue.text + if (newText.isEmpty()) { + textFieldValue = newTextFieldValue + } else if (newText.all { it.isDigit() }) { + // Safely parse to Long to avoid NumberFormatException on large numbers + val number = newText.toLongOrNull() + if (number != null && number <= maxValue) { + textFieldValue = newTextFieldValue + } else { + // The number is too large or invalid, show a toast + Toast.makeText( + context, + "Input Number exceeded max value of $maxValue", + Toast.LENGTH_SHORT + ).show() + } + } + }, + //label = { Text("Count") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, // KeyboardType.Number, //KeyboardType.Text + imeAction = ImeAction.Done, + //autoCorrect = false + ), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + performSetClick() + }), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester) + .background(Color.Transparent) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + + } + + HorizontalDivider(thickness = 1.px.dp, color = Color(0,0,0).copy(alpha = 0.38f)) + } + } + } else { + // External + Row( + modifier = Modifier + .fillMaxWidth() + .height( 100.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Read", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .background(Color(245,245,245)) + .border(width = 1.px.dp, Color.LightGray,), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = " %d".format(measured), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 16.px.sp, + color = Color.Black.copy(alpha = 0.5f) + ) + } + + HorizontalDivider(thickness = 1.px.dp, color = Color(0,0,0).copy(alpha = 0.38f)) + } + + VerticalDivider( + color = Color(209, 209, 209), + thickness = 1.px.dp, + modifier = Modifier + .padding(top = 20.px.dp, bottom = 10.px.dp, start = 10.px.dp, end = 10.px.dp) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Write", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .background(Color(245,245,245)) + .border(width = 1.px.dp, Color.LightGray,), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = textFieldValue, + onValueChange = { newTextFieldValue -> + val newText = newTextFieldValue.text + if (newText.isEmpty() || + (newText.length < 10 && newText.all { char -> char.isDigit() }) + ) { + // Update the state with the new TextFieldValue object + textFieldValue = newTextFieldValue + } else { + Toast.makeText( + context, + "Input Number exceeded max value ${maxValue}", + Toast.LENGTH_SHORT + ).show() + } + }, + //label = { Text("Count") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, // KeyboardType.Number, //KeyboardType.Text + imeAction = ImeAction.Done, + //autoCorrect = false + ), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + performSetClick() + }), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester) + .background(Color.Transparent) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + + } + + HorizontalDivider(thickness = 1.px.dp, color = Color(0,0,0).copy(alpha = 0.38f)) + } + } + } + + Spacer(Modifier.weight(1f)) + + // Read & Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Read + TextButton( + modifier = Modifier + .clip(RoundedCornerShape(24.px.dp)) + .background(Color(243,205,81)) + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Read Clicked") + onClickRead() + } + ) { + Text( + text = "Read", + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + Spacer(Modifier.weight(1f)) + + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onLoadClick") + onClickCancel() + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Set + TextButton( + modifier = Modifier + .clip(RoundedCornerShape(24.px.dp)) + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(47, 51,57), + 1.0f to Color(83, 88, 97), + ) + ) + ) + .border( + width = 1.5.px.dp, + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(92, 99,107), + 1.0f to Color(34, 37, 40), + ) + ), + shape = RoundedCornerShape(24.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(24.px.dp), + color = Color(243,205,81).copy(alpha = 0.2f), + blur = 3.5.px.dp, + offsetX = 0.px.dp, + offsetY = 0.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(24.px.dp), + color = Color(0,0,0).copy(alpha = 0.1f), + blur = 1.75.px.dp, + offsetX = 0.px.dp, + offsetY = 1.75.px.dp, + spread = 0.px.dp + ) + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + performSetClick() + } + ) { + Text( + text = "Set", + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.White + ) + } + } + + } + } + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + //device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewRefer1Popup( + /* + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) + */ +) { + Box(modifier = Modifier.fillMaxSize().background(Color.White)) { + Refer1Popup( + title = stringResource(R.string.energy_detect_refer1_title), + ) + } +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/Refer2Popup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/Refer2Popup.kt new file mode 100644 index 0000000..44f17c1 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/energy/Refer2Popup.kt @@ -0,0 +1,560 @@ +package com.laseroptek.raman.ui.screens.engineer.energy + +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.MAX_REFER2_VALUE +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.Refer2 +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Refer2Popup( + title: String = stringResource(R.string.energy_detect_refer2_title), + refer2: Refer2 = Refer2(), + maxValue: Int = MAX_REFER2_VALUE, + onClick: (Boolean, Refer2) -> Unit = { _, _ -> }, +) { + val context = LocalContext.current + var goodFieldValue by remember { + mutableStateOf( + // Initialize TextFieldValue with the text and set the cursor to the end. + TextFieldValue( + text = "%d".format((refer2.good * 100).toInt()), + selection = TextRange("%d".format((refer2.good * 100).toInt()).length) // This places the cursor at the end + ) + ) + } + var acceptableFieldValue by remember { + mutableStateOf( + // Initialize TextFieldValue with the text and set the cursor to the end. + TextFieldValue( + text = "%d".format((refer2.acceptable*100).toInt()), + selection = TextRange("%d".format((refer2.acceptable*100).toInt()).length) // This places the cursor at the end + ) + ) + } + var badFieldValue by remember { + mutableStateOf( + // Initialize TextFieldValue with the text and set the cursor to the end. + TextFieldValue( + text = "%d".format((refer2.bad*100).toInt()), + selection = TextRange("%d".format((refer2.bad*100).toInt()).length) // This places the cursor at the end + ) + ) + } + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + val performOkClick = { + if (goodFieldValue.text.isEmpty() || acceptableFieldValue.text.isEmpty() || badFieldValue.text.isEmpty()) { + Toast.makeText( + context, + "Input Number", + Toast.LENGTH_SHORT + ).show() + } else { + // don't Hide the keyboard first for a clean UI transition + //focusManager.clearFocus() + + Timber.d("OK action performed ${goodFieldValue.text}, ${acceptableFieldValue.text}, ${badFieldValue.text}") + + onClick( + true, + Refer2( + good = goodFieldValue.text.toFloatOrNull() ?: 0f, + acceptable = acceptableFieldValue.text.toFloatOrNull() ?: 0f, + bad = badFieldValue.text.toFloatOrNull() ?: 0f, + ) + ) // This now closes the popup via the parent + } + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + FullScreen { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 50.px.dp), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .size(420.px.dp, 360.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(start = 24.px.dp, end = 24.px.dp, top = 10.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 24.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + Spacer(Modifier.height(30.px.dp)) + + // Good + Row( + modifier = Modifier + .size(372.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + // Text - Good + Row( + modifier = Modifier + .size(100.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Good", + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + } + + // Good (Set) + Row( + modifier = Modifier + .size(222.px.dp, 56.px.dp) + .background(Color(245,245,245)) + .border(width = 1.px.dp, Color.LightGray,), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = goodFieldValue, + onValueChange = { newTextFieldValue -> + val newText = newTextFieldValue.text + if (newText.isEmpty()) { + goodFieldValue = newTextFieldValue + } else if (newText.all { it.isDigit() }) { + // Safely parse to Long to avoid NumberFormatException on large numbers + val number = newText.toLongOrNull() + if (number != null && number <= maxValue) { + goodFieldValue = newTextFieldValue + } else { + // The number is too large or invalid, show a toast + Toast.makeText( + context, + "Input Number exceeded max value of $maxValue", + Toast.LENGTH_SHORT + ).show() + } + } + }, + //label = { Text("Count") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, // KeyboardType.Number, //KeyboardType.Text + imeAction = ImeAction.Done, + //autoCorrect = false + ), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + performOkClick() + }), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + } + + // Text - % + Row( + modifier = Modifier + .size(50.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "%", + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 16.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + } + } + + Spacer(Modifier.height(5.px.dp)) + + // Acceptable + Row( + modifier = Modifier + .size(372.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + // Text - Acceptable + Row( + modifier = Modifier + .size(100.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Acceptable", + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + } + + // Acceptable (Set) + Row( + modifier = Modifier + .size(222.px.dp, 56.px.dp) + .background(Color(245,245,245)) + .border(width = 1.px.dp, Color.LightGray,), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = acceptableFieldValue, + onValueChange = { newTextFieldValue -> + val newText = newTextFieldValue.text + if (newText.isEmpty()) { + acceptableFieldValue = newTextFieldValue + } else if (newText.all { it.isDigit() }) { + // Safely parse to Long to avoid NumberFormatException on large numbers + val number = newText.toLongOrNull() + if (number != null && number <= maxValue) { + acceptableFieldValue = newTextFieldValue + } else { + // The number is too large or invalid, show a toast + Toast.makeText( + context, + "Input Number exceeded max value of $maxValue", + Toast.LENGTH_SHORT + ).show() + } + } + }, + //label = { Text("Count") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, // KeyboardType.Number, //KeyboardType.Text + imeAction = ImeAction.Done, + //autoCorrect = false + ), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + performOkClick() + }), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + } + + // Text - % + Row( + modifier = Modifier + .size(50.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "%", + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 16.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + } + } + + Spacer(Modifier.height(5.px.dp)) + + // Bad + Row( + modifier = Modifier + .size(372.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + // Text - Bad + Row( + modifier = Modifier + .size(100.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Bad", + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + } + + // Bad (Set) + Row( + modifier = Modifier + .size(222.px.dp, 56.px.dp) + .background(Color(245,245,245)) + .border(width = 1.px.dp, Color.LightGray,), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = badFieldValue, + onValueChange = { newTextFieldValue -> + val newText = newTextFieldValue.text + if (newText.isEmpty()) { + badFieldValue = newTextFieldValue + } else if (newText.all { it.isDigit() }) { + // Safely parse to Long to avoid NumberFormatException on large numbers + val number = newText.toLongOrNull() + if (number != null && number <= maxValue) { + badFieldValue = newTextFieldValue + } else { + // The number is too large or invalid, show a toast + Toast.makeText( + context, + "Input Number exceeded max value of $maxValue", + Toast.LENGTH_SHORT + ).show() + } + } + }, + //label = { Text("Count") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, // KeyboardType.Number, //KeyboardType.Text + imeAction = ImeAction.Done, + //autoCorrect = false + ), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + performOkClick() + }), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester) + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + } + + // Text - % + Row( + modifier = Modifier + .size(50.px.dp, 56.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "%", + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 16.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + } + } + + Spacer(Modifier.weight(1f)) + + // Cancel & Set + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onLoadClick") + onClick( + false, + Refer2( + good = goodFieldValue.text.toFloat(), + acceptable = acceptableFieldValue.text.toFloat(), + bad = badFieldValue.text.toFloat(), + + ) + ) // This now closes the popup via the parent + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + + // Set + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + performOkClick() + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } + + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewRefer2Popup( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + Refer2Popup( + //mainViewModel = mainViewModel + title = stringResource(R.string.energy_detect_refer2_title), + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/export/ExportConfirmPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/export/ExportConfirmPopup.kt new file mode 100644 index 0000000..e91b22c --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/export/ExportConfirmPopup.kt @@ -0,0 +1,220 @@ +package com.laseroptek.raman.ui.screens.engineer.export + +import android.content.res.Configuration +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.repository.datastore +import com.laseroptek.raman.ui.MainActivity +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.PreferenceBackupManager +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.launch +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +@Composable +fun ExportConfirmPopup( + title: String = stringResource(R.string.export_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + val context = LocalContext.current + val activity = LocalContext.current as? MainActivity + + var isLoading by remember { mutableStateOf(false) } + //val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + val preferenceBackupManager = PreferenceBackupManager(context, context.datastore) + + val createFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/json"), // MIME type for a text file + onResult = { uri: Uri? -> + uri?.let { selectedUri -> + // Get a reference to the MainActivity to call its public functions + //activity?.setSystemBarsVisible(false) + + isLoading = true + coroutineScope.launch { + val success = preferenceBackupManager.exportPreferencesToUri( + uri = selectedUri + ) + isLoading = false + val message = if (success) "File exported successfully!" else "Export failed." + //snackbarHostState.showSnackbar(message, duration = SnackbarDuration.Short) + Timber.d("Export result: $success to $selectedUri") + onClick(true) + } + } ?: run { + Timber.d("Export cancelled by user.") + + //coroutineScope.launch { + // snackbarHostState.showSnackbar("Export cancelled.", duration = SnackbarDuration.Short) + //} + + onClick(false) + } + } + ) + + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(start = 40.px.dp, end = 40.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Start, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + if (isLoading) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(16.dp)) + Text("Exporting...") + } else { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + + // Before launching the system UI, show the navigation bar. + //activity?.setSystemBarsVisible(true) + + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val fileName = "export_$timestamp" // Suggest a .txt extension + createFileLauncher.launch(fileName) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false +) +@Composable +fun PreviewExportConfirmPopup() { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ){ + ExportConfirmPopup( + title = stringResource(R.string.export_confirm_title), + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/import/ImportConfirmPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/import/ImportConfirmPopup.kt new file mode 100644 index 0000000..78eca03 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/import/ImportConfirmPopup.kt @@ -0,0 +1,221 @@ +package com.laseroptek.raman.ui.screens.engineer.import + +import android.content.res.Configuration +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.repository.datastore +import com.laseroptek.raman.ui.MainActivity +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.PreferenceBackupManager +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.launch +import timber.log.Timber + +@Composable +fun ImportConfirmPopup( + title: String = stringResource(R.string.import_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + val context = LocalContext.current + val activity = LocalContext.current as? MainActivity + + var isLoading by remember { mutableStateOf(false) } + var fileContentPreview by remember { mutableStateOf(null) } // To show a snippet of imported content + //val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + val preferenceBackupManager = PreferenceBackupManager(context, context.datastore) + + val openFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), // Contract for opening an existing document + onResult = { uri: Uri? -> + uri?.let { selectedUri -> + isLoading = true + fileContentPreview = null // Clear previous preview + coroutineScope.launch { + val result = preferenceBackupManager.importPreferencesFromUri(uri = selectedUri, clearExisting = true) + isLoading = false + if (result) { + //snackbarHostState.showSnackbar("File content loaded from $selectedUri", duration = SnackbarDuration.Short) + Timber.d("File content loaded from $selectedUri") + onClick(true) + } else { + //snackbarHostState.showSnackbar("Failed to read file.", duration = SnackbarDuration.Short) + Timber.d("Failed to read file from $selectedUri") + onClick(false) + } + } + } ?: run { + Timber.d("Import cancelled by user.") + coroutineScope.launch { + //snackbarHostState.showSnackbar("Import cancelled.", duration = SnackbarDuration.Short) + onClick(false) + } + } + } + ) + + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(start = 40.px.dp, end = 40.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Start, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + if (isLoading) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(16.dp)) + Text("Importing...") + } else { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + // Before launching the system UI, show the navigation bar. + //activity?.setSystemBarsVisible(true) + + // Launch the file picker, filtering for text files. + // You can specify multiple MIME types: arrayOf("text/plain", "application/json") + openFileLauncher.launch(arrayOf("text/plain", "application/json")) // Example: text files or any file + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + fileContentPreview?.let { + Spacer(modifier = Modifier.height(24.dp)) + Text("File Preview (first 200 chars):", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + maxLines = 10, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 200.dp) // Limit preview height + ) + } + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewImportConfirmPopup() { + remember { mutableStateOf(true) } + ImportConfirmPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/load/LoadConfirmPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/load/LoadConfirmPopup.kt new file mode 100644 index 0000000..d1051d9 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/load/LoadConfirmPopup.kt @@ -0,0 +1,143 @@ +package com.laseroptek.raman.ui.screens.engineer.load + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun LoadConfirmPopup( + title: String = stringResource(R.string.load_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(start = 30.px.dp, end = 30.px.dp, bottom = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewLoadConfirmPopup() { + remember { mutableStateOf(true) } + LoadConfirmPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/log/LogPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/log/LogPopup.kt new file mode 100644 index 0000000..e9cc473 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/log/LogPopup.kt @@ -0,0 +1,437 @@ +package com.laseroptek.raman.ui.screens.engineer.log + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.CMD.CMD_FLAG +import com.laseroptek.raman.const.CMD.LASER_STATUS +import com.laseroptek.raman.const.CMD.TEMPERATURE +import com.laseroptek.raman.const.CMD.WARNING +import com.laseroptek.raman.const.CMD.WRITE_FLAG +import com.laseroptek.raman.data.source.db.model.SerialLog +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.mask +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.ext.simpleVerticalScrollbar +import com.laseroptek.raman.utils.ext.toHHMMSSString +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalStdlibApi::class) +@Composable +fun LogPopup( + title: String = stringResource(R.string.log_title), + serialLogList: StateFlow> = MutableStateFlow>(emptyList()), + onClick: (Boolean) -> Unit = {}, +) { + FullScreen(contentAlignment = Alignment.CenterEnd) { + val selectedIndex = remember { mutableStateOf(0) } + val serialLogListCollect by serialLogList.collectAsState() + Timber.d("serialLogListCollect.size: ${serialLogListCollect.size}") + + val filteredSerialLogs = remember(serialLogListCollect, selectedIndex.value) { + when(selectedIndex.value) { + 0 -> serialLogListCollect + 1 -> serialLogListCollect.filter { it.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) == LASER_STATUS } + 2 -> serialLogListCollect.filter { it.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) == TEMPERATURE } + 3 -> serialLogListCollect.filter { it.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) == WARNING } + else -> serialLogListCollect + } + } + + //Timber.d("serialLogs: ${serialLogs.toList()}") + + filteredSerialLogs.forEachIndexed { index, log -> + Timber.d("index: $index -> id: ${log.id}") + Timber.d("index: $index -> ts: ${log.ts}") + Timber.d("index: $index -> desc: ${log.desc}") + } + + Row(modifier = Modifier + .fillMaxHeight() + .width(640.px.dp) + .clip(RoundedCornerShape(16.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(5.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Top + ) { + // Radio Buttons + Column( + modifier = Modifier + .fillMaxHeight() + .width(180.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.px.dp) + .padding(start = 18.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 16.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.weight(1f)) + } + + // All + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + RadioButton( + selected = selectedIndex.value == 0, + onClick = { selectedIndex.value = 0 } + ) + Text( + text = stringResource(R.string.all), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Laser Status + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + RadioButton( + selected = selectedIndex.value == 1, + onClick = { selectedIndex.value = 1 } + ) + Text( + text = stringResource(R.string.laser_status), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Temperature + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + RadioButton( + selected = selectedIndex.value == 2, + onClick = { selectedIndex.value = 2 } + ) + Text( + text = stringResource(R.string.temperature), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Alarm + Row( + modifier = Modifier + .fillMaxWidth() + .size(120.px.dp, 56.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + RadioButton( + selected = selectedIndex.value == 3, + onClick = { selectedIndex.value = 3 } + ) + Text( + text = stringResource(R.string.alarm), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Thin, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + } + + // Vertical Divider + Column { + VerticalDivider(color = Color.Gray, thickness = 1.dp) + } + + // Log Column + Column( + modifier = Modifier + .fillMaxHeight() + .width(480.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.px.dp) + .padding(start = 18.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = stringResource(R.string.log_data), + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 16.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.weight(1f)) + + IconButton( + onClick = { onClick.invoke(false) }, + modifier = Modifier.size(40.px.dp, 40.px.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, + modifier = Modifier.size(24.px.dp), + contentScale = ContentScale.Crop + ) + } + } + + // String Lists. + LogList( + serialLogList = filteredSerialLogs, + modifier = Modifier.height(552.px.dp), + selectedIndex = selectedIndex + ) + + // Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + /* + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onLoadClick") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + */ + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.close), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } + + } +} + + +@Composable +fun LogRow( + serialLog: SerialLog = SerialLog(), + index: Int, + onItemClickListener: (Int) -> Unit, +) { + Row( + modifier = Modifier + .noRippleClickable { onItemClickListener(index) } + .fillMaxWidth() + .padding(16.dp) + .background( + color = if (index == 0) Color(222, 173, 11).copy(alpha = 0.1f) else Color.Transparent + ), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.Start + ) { + // Timestamp. + Column { + Text( + text = serialLog.ts.toHHMMSSString(), + style = RobotoTypography.bodyMedium, + fontSize = 12.px.sp, + fontWeight = FontWeight.ExtraLight, + color = Color.Black + ) + + Text( + text = " | ", + style = RobotoTypography.bodyMedium, + fontSize = 12.px.sp, + fontWeight = FontWeight.ExtraLight, + color = Color.Black + ) + } + + Spacer(Modifier.width(10.px.dp)) + + // Log. + Column { + Text( + text = serialLog.desc, + style = RobotoTypography.bodyMedium, + fontSize = 12.px.sp, + fontWeight = FontWeight.ExtraLight, + color = Color.Black + ) + } + + Spacer(Modifier.weight(1f)) + } +} + +@Composable +fun LogList( + modifier: Modifier = Modifier, + serialLogList: List = emptyList(), + selectedIndex: MutableState = mutableStateOf(0), +) { + val state = rememberLazyListState() + LazyColumn( + modifier = modifier.simpleVerticalScrollbar(state), + state = state + ) { + itemsIndexed(serialLogList) { index, serialLog -> + LogRow( + serialLog, + index, + onItemClickListener = { idx -> + Timber.d("String Item Clicked(${idx})") + selectedIndex.value = idx + }, + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) + +@Composable +fun PreviewLogView() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + LogPopup() + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/qswitch/QSwitchView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/qswitch/QSwitchView.kt new file mode 100644 index 0000000..2f661a3 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/qswitch/QSwitchView.kt @@ -0,0 +1,179 @@ +package com.laseroptek.raman.ui.screens.engineer.qswitch + +import android.content.res.Configuration +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.const.MAX_Q_SWITCH_VALUE +import com.laseroptek.raman.const.MIN_Q_SWITCH_VALUE +import com.laseroptek.raman.data.model.serial.QSwitch +import com.laseroptek.raman.data.model.serial.Q_SWITCH_TOGGLE +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun QSwitchView( + modifier: Modifier = Modifier, + qSwitch: QSwitch = QSwitch(), + onClick: (QSwitch) -> Unit = {}, + title:String = "Q-S/W(main)" +) { + var qSwitchToggle by remember { mutableStateOf(0) } // 0: +0.5, 1: +10, 2: -10, 3: -0.5 + //val qSwitchSubToggle = mutableStateOf(0) // 0: +0.5, 1: +10, 2: -10, 3: -0.5 + + fun onValueChange(currentValue: Float, createNewQSwitch: (Float) -> QSwitch) { + val newValue = when (qSwitchToggle) { + 0 -> currentValue + 0.5f + 1 -> currentValue + 10.0f + 2 -> currentValue - 10.0f + 3 -> currentValue - 0.5f + else -> currentValue + } + + // Clamp the value to ensure it stays within the valid range (e.g., 0 to 250) + val clampedValue = newValue.coerceIn(MIN_Q_SWITCH_VALUE, MAX_Q_SWITCH_VALUE) + + // Only call onClick if the value has actually changed + if (clampedValue != currentValue) { + onClick(createNewQSwitch(clampedValue)) + } + } + + Row(modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // Q-S/W - title + Column(modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(30.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Normal, + fontSize = 12.px.sp, + color = Color.Black + ) + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + // Q-S/W - Delay + Column( + modifier = Modifier + .noRippleClickable(onClick = { + onValueChange(qSwitch.delayTime) { newDelay -> + qSwitch.copy(delayTime = newDelay) + } + }) + .fillMaxWidth() + .weight(1f) + .height(30.px.dp) + .border(width = 1.px.dp, color = Color.Gray, RoundedCornerShape(5.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Delay: %.1f μs".format(qSwitch.delayTime), //"DELAY : -.- μs", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 11.px.sp, + color = Color.Black + ) + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + // Q-S/W - Interval + Column( + modifier = Modifier + .noRippleClickable(onClick = { + onValueChange(qSwitch.intervalTime) { newDelay -> + qSwitch.copy(intervalTime = newDelay) + } + }) + .fillMaxWidth() + .weight(1f) + .height(30.px.dp) + .border(width = 1.px.dp, color = Color.Gray, RoundedCornerShape(5.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Interval: %.1f μs".format(qSwitch.intervalTime), //"INTERVAL : -.- μs", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 11.px.sp, + color = Color.Black + ) + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + // Q-S/W toggle + Column( + modifier = Modifier + .noRippleClickable(onClick = { + qSwitchToggle = (qSwitchToggle+1) % 4 + Timber.d("onClick > qSwitchToggle.value : ${qSwitchToggle}") + }) + .size(40.px.dp, 30.px.dp) + .border(width = 1.px.dp, color = Color.Gray, RoundedCornerShape(5.px.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = Q_SWITCH_TOGGLE.get(qSwitchToggle), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 11.px.sp, + color = Color.Black + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=60dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewQSwitchView() { + Row { + QSwitchView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + title = "Q-S/W (main)" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/save/SaveConfirmPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/save/SaveConfirmPopup.kt new file mode 100644 index 0000000..c9c1bba --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/save/SaveConfirmPopup.kt @@ -0,0 +1,143 @@ +package com.laseroptek.raman.ui.screens.engineer.save + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun SaveConfirmPopup( + title: String = stringResource(R.string.save_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(start = 30.px.dp, end = 30.px.dp, bottom = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSaveConfirmPopup() { + remember { mutableStateOf(true) } + SaveConfirmPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/serial/SerialNumberPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/serial/SerialNumberPopup.kt new file mode 100644 index 0000000..04ae8cb --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/serial/SerialNumberPopup.kt @@ -0,0 +1,538 @@ +package com.laseroptek.raman.ui.screens.engineer.serial + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.productMonthLists +import com.laseroptek.raman.const.productQuantityHundredthRangeList +import com.laseroptek.raman.const.productYearOrQuantityOnethRangeList +import com.laseroptek.raman.const.productYearOrQuantityTenthRangeList +import com.laseroptek.raman.ui.common.core.WheelPickerDefaults +import com.laseroptek.raman.ui.common.core.WheelTextPicker +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SerialNumberPopup( + title: String = stringResource(R.string.serial_number), + productSerialList: MutableList = mutableListOf(), + laserHandSerialList: MutableList = mutableListOf(), + powerSupplySerialList: MutableList = mutableListOf(), + onClick: (Boolean, List, List, List) -> Unit = { _, _, _, _ -> }, +) { + FullScreen(contentAlignment= Alignment.CenterEnd) { + Column( + modifier = Modifier + .fillMaxHeight() + .width(360.px.dp) + .clip(RoundedCornerShape(16.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(5.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.px.dp) + .padding(start = 18.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.ExtraLight, + fontSize = 16.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.weight(1f)) + + IconButton( + onClick = { onClick(false, productSerialList, laserHandSerialList, powerSupplySerialList) }, + modifier = Modifier.size(40.px.dp, 40.px.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, + modifier = Modifier.size(24.px.dp), + contentScale = ContentScale.Crop + ) + } + } + + // Product Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 16.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Bottom + ){ + Text( + text = stringResource(R.string.product), + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Medium, + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + } + // Product SN + SN_Layer(serialList = productSerialList) + + // Divider + HorizontalDivider( + modifier = Modifier.padding(start = 18.px.dp, end=10.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // LaserHand Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 16.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Bottom + ){ + Text( + text = stringResource(R.string.laser_hand), + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Medium, + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + } + // LaserHand SN + SN_Layer(serialList = laserHandSerialList) + + // Divider + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=10.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + // Power Supply Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 16.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Bottom + ){ + Text( + text = stringResource(R.string.powser_supply), + style = RobotoTypography.labelMedium, + fontWeight = FontWeight.Medium, + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + } + + // Power Supply SN + SN_Layer(serialList = powerSupplySerialList) + + // Divider + HorizontalDivider( + modifier = Modifier + .padding(start = 18.px.dp, end=10.px.dp), + thickness = 0.2.px.dp, + color = Color.LightGray + ) + + Spacer(Modifier.weight(1f)) + + // Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onLoadClick") + onClick(false, productSerialList, laserHandSerialList, powerSupplySerialList) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true, productSerialList, laserHandSerialList, powerSupplySerialList) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } +} + + +@Composable +fun SN_Layer( + serialList: MutableList = mutableListOf("C","N","2","C","C","2") +) { + // Prep & Wheel + Row( + modifier = Modifier + .fillMaxWidth() + .height(120.px.dp) + .padding(start = 18.px.dp, end=10.px.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // LO-3ND + Column( + modifier = Modifier + .fillMaxHeight() + .width(60.px.dp), + verticalArrangement = Arrangement.Center + ) { + Text( + text = "LO-3ND", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 13.px.sp, + ) + } + + // - + Column( + modifier = Modifier + .fillMaxHeight() + .width(8.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "-", + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + ) + } + + // 제조년도 (1) + Column( + modifier = Modifier + .fillMaxHeight() + .width(30.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + WheelTextPicker( + modifier = Modifier.size(30.px.dp, 120.px.dp), + startIndex = productYearOrQuantityTenthRangeList.indexOf(serialList[0]).takeIf {it != -1} ?: 0, + size = DpSize(30.px.dp, 120.px.dp), + texts = productYearOrQuantityTenthRangeList, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + + serialList[0] = productYearOrQuantityTenthRangeList[snappedIndex] + + Timber.d("serialList[0]: ${serialList[0]}") + return@WheelTextPicker snappedIndex + } + ) + } + + // 제조년도 (2) + Column( + modifier = Modifier + .fillMaxHeight() + .width(30.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + WheelTextPicker( + modifier = Modifier.size(30.px.dp, 120.px.dp), + startIndex = productYearOrQuantityOnethRangeList.indexOf(serialList[1]).takeIf {it != -1} ?: 0, + size = DpSize(30.px.dp, 120.px.dp), + texts = productYearOrQuantityOnethRangeList, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + + serialList[1] = productYearOrQuantityOnethRangeList[snappedIndex] + + Timber.d("serialList[1]: ${serialList[1]}") + return@WheelTextPicker snappedIndex + } + ) + } + + // - + Column( + modifier = Modifier + .fillMaxHeight() + .width(8.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "-", + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + ) + } + + // 생산수량 (1) + Column( + modifier = Modifier + .fillMaxHeight() + .width(30.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + WheelTextPicker( + modifier = Modifier.size(30.px.dp, 120.px.dp), + startIndex = productQuantityHundredthRangeList.indexOf(serialList[2]).takeIf {it != -1} ?: 0, + size = DpSize(30.px.dp, 120.px.dp), + texts = productQuantityHundredthRangeList, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + + serialList[2] = productQuantityHundredthRangeList[snappedIndex] + + Timber.d("serialList[2]: ${serialList[2]}") + return@WheelTextPicker snappedIndex + } + ) + } + + // 생산수량 (2) + Column( + modifier = Modifier + .fillMaxHeight() + .width(30.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + WheelTextPicker( + modifier = Modifier.size(30.px.dp, 120.px.dp), + startIndex = productYearOrQuantityTenthRangeList.indexOf(serialList[3]).takeIf {it != -1} ?: 0, + size = DpSize(80.px.dp, 120.px.dp), + texts = productYearOrQuantityTenthRangeList, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + + serialList[3] = productYearOrQuantityTenthRangeList[snappedIndex] + + Timber.d("serialList[3]: ${serialList[3]}") + return@WheelTextPicker snappedIndex + } + ) + } + + // 생산수량 (3) + Column( + modifier = Modifier + .fillMaxHeight() + .width(30.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + WheelTextPicker( + modifier = Modifier.size(30.px.dp, 120.px.dp), + startIndex = productYearOrQuantityOnethRangeList.indexOf(serialList[4]).takeIf {it != -1} ?: 0, + size = DpSize(80.px.dp, 120.px.dp), + texts = productYearOrQuantityOnethRangeList, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + + serialList[4] = productYearOrQuantityOnethRangeList[snappedIndex] + + Timber.d("serialList[4]: ${serialList[4]}") + return@WheelTextPicker snappedIndex + } + ) + } + + // - + Column( + modifier = Modifier + .fillMaxHeight() + .width(8.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "-", + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + ) + } + + // 제조월 + Column( + modifier = Modifier + .fillMaxHeight() + .width(30.px.dp) + , verticalArrangement = Arrangement.Center + , horizontalAlignment = Alignment.CenterHorizontally + ) { + WheelTextPicker( + modifier = Modifier.size(30.px.dp, 120.px.dp), + startIndex = productMonthLists.indexOf(serialList[5]).takeIf {it != -1} ?: 0, + size = DpSize(30.px.dp, 120.px.dp), + texts = productMonthLists, + rowCount = 3, + style = RobotoTypography.bodyMedium.copy(fontSize = 14.px.sp), + color = Color.Black, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + color = Color(222,173,11), + width = 1.0f + ), + onScrollFinished = { snappedIndex -> + Timber.d("snappedIndex: $snappedIndex") + + serialList[5] = productMonthLists[snappedIndex] + + Timber.d("serialList[5]: ${serialList[5]}") + return@WheelTextPicker snappedIndex + } + ) + } + } +} + + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSerialNumberView() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SerialNumberPopup() + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/set/SetConfirmPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/set/SetConfirmPopup.kt new file mode 100644 index 0000000..d16aa94 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/set/SetConfirmPopup.kt @@ -0,0 +1,143 @@ +package com.laseroptek.raman.ui.screens.engineer.set + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun SetConfirmPopup( + title: String = stringResource(R.string.set_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(top = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSetConfirmPopup() { + remember { mutableStateOf(true) } + SetConfirmPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/standby/EMStandByPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/standby/EMStandByPopup.kt new file mode 100644 index 0000000..7d01138 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/standby/EMStandByPopup.kt @@ -0,0 +1,355 @@ +package com.laseroptek.raman.ui.screens.engineer.standby + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.model.serial.LaserStatus +import com.laseroptek.raman.data.model.serial.getLaserStatusString +import com.laseroptek.raman.ui.common.fullscreen.FullScreenPopup +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + + +@Composable +fun EMStandByPopup( + title: String = stringResource(R.string.stand_by_title), + description: String= "", + laserStatus: StateFlow = MutableStateFlow(LaserStatus()), + laserCount: Int = 0, + onClick: () -> Unit = {} +) { + FullScreenPopup( + //showPopup = showEmStandByPopup, + backgroundColor = Color(120,120,120), + onDismissRequest = { + onClick() + } + ) { + val laserStatus by laserStatus.collectAsState() + //val preset by preset.collectAsState() + + Column( + modifier = Modifier + .size(458.px.dp, 488.px.dp) + .clip(RoundedCornerShape(32.px.dp)) + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // EMStandBy (Title) + Row( + modifier = Modifier + .fillMaxWidth() + .height(96.px.dp) + .clip(RoundedCornerShape(topStart = 32.px.dp, topEnd = 32.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(34, 197, 94), + Color(22, 163, 74) + ), + //start = Offset(0f, 0f), // Optional: Start at the top-left + //end = Offset(200f, 200f) // Optional: End at the bottom-right + ) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_round_check), + contentDescription = null, + modifier = Modifier + .size(28.px.dp), + contentScale = ContentScale.Crop, + colorFilter = ColorFilter.tint(Color.White) + ) + + Spacer(Modifier.height(3.px.dp)) + + Text( + text = getLaserStatusString(laserStatus.laserStatus), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 20.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + } + + } + + // PULSE | FLUENCE | REPETION + Row( + modifier = Modifier + .fillMaxWidth() + .height(120.px.dp) + .background(Color.White) + .padding( + start = 20.px.dp, end = 20.px.dp, top = 30.px.dp, bottom = 30.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // PULSE -> (OnTime : xx.x) + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.pulse_duration), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + + Text( + text = "%.1fms".format(laserStatus.onTime), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 20.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Divider + Column { + VerticalDivider(color = Color.Gray, thickness = 1.dp) + } + + // FLUENCE -> (voltage: xxx) + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.fluence), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Text( + text = "%.1fJ/cm²".format(laserStatus.frequence), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 20.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Divider + Column { + VerticalDivider(color = Color.Gray, thickness = 1.dp) + } + + // REPETITION -> (frequence: x.x) + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.repetition), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Text( + text = "%.1fHz".format(laserStatus.frequence), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 20.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + } + + // laser count & foot switch border + Column( + modifier = Modifier + .size(410.px.dp, 136.px.dp) + .border( + width = 1.px.dp, + color = Color(209, 209, 209), + RoundedCornerShape(12.px.dp) + ), + ) { + // Laser Count + Column ( + modifier = Modifier + .fillMaxWidth() + .height(100.px.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(238, 238, 238) + ), + //start = Offset(0f, 0f), // Optional: Start at the top-left + //end = Offset(200f, 200f) // Optional: End at the bottom-right + ), + RoundedCornerShape( + topStart = 12.px.dp, + topEnd = 12.px.dp + ) + ) + .clip( + RoundedCornerShape( + topStart = 12.px.dp, + topEnd = 12.px.dp + ) + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ){ + Text( + text = stringResource(R.string.laser_count_title), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + + Text( + text = "%06d".format(laserCount), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 36.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // foot switch text + Column( + modifier = Modifier + .fillMaxWidth() + .height(36.px.dp) + .background(Color.White) + .clip( + RoundedCornerShape( + bottomStart = 12.px.dp, + bottomEnd = 12.px.dp + ) + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.foot_switch_count_title), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 12.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + } + + // alert logo and message + Row(modifier = Modifier + .fillMaxWidth() + .height(70.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_alert), + contentDescription = null, + modifier = Modifier.size(18.px.dp), + ) + Spacer(modifier = Modifier.width(10.px.dp)) + Text( + text = description, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 12.px.sp, + color = Color.Red, + textAlign = TextAlign.Center + ) + } + + // back to EMStandBy | Ready + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.px.dp) + .padding(end = 20.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + modifier = Modifier + .size(153.px.dp, 40.px.dp) + .clip(RoundedCornerShape(24.px.dp)) + .background(color = Color(35, 35, 35)), + onClick = { + onClick() + } + ) { + Text( + text = stringResource(R.string.back_to_stand_by), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Medium, + fontSize = 14.px.sp, + color = Color(243,205,81) + ) + } + } + } + } +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/update/UpdateConfirmPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/update/UpdateConfirmPopup.kt new file mode 100644 index 0000000..a96e310 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/update/UpdateConfirmPopup.kt @@ -0,0 +1,141 @@ +package com.laseroptek.raman.ui.screens.engineer.update + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun UpdateConfirmPopup( + title: String = stringResource(R.string.update_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(start = 30.px.dp, end = 30.px.dp, bottom = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewUpdateConfirmPopup() { + //val showPopup = remember { mutableStateOf(true) } + UpdateConfirmPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/version/ProgramVersionPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/version/ProgramVersionPopup.kt new file mode 100644 index 0000000..845e1c2 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/engineer/version/ProgramVersionPopup.kt @@ -0,0 +1,255 @@ +package com.laseroptek.raman.ui.screens.engineer.version + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.model.serial.Version +import com.laseroptek.raman.ui.common.fullscreen.FullScreenPopup +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.formatWithDots +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProgramVersionPopup( + title: String = stringResource(R.string.software_information_title), + version: StateFlow = MutableStateFlow(Version()), + onClick: () -> Unit = {}, +) { + val version by version.collectAsState() + + FullScreenPopup { + Column( + modifier = Modifier + .size(330.px.dp, 304.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Software version + Row( + modifier = Modifier + .fillMaxWidth() + .height(72.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.bodySmall.copy( + fontWeight = FontWeight.Thin, + ), + fontWeight = FontWeight.Thin, + fontSize = 22.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + Column( + modifier = Modifier + .size(282.px.dp, 144.px.dp) + .clip(RoundedCornerShape(12.px.dp)) + .background(Color(250, 250, 250)) + , + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + // Product + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.px.dp) + .padding(start = 15.px.dp, end = 15.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Product", + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.weight(1f)) + Text( + text = "Code: %s, Name: %s".format(version.verCode.formatWithDots(), version.verName), + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + ) + } + + HorizontalDivider( + modifier = Modifier.padding(start = 16.px.dp, end=16.px.dp), + thickness = 0.1.px.dp, + color = Color.Gray + ) + + // Laser hand + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.px.dp) + .padding(start = 15.px.dp, end = 15.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Laser Hand", + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.weight(1f)) + Text( + text = "H/W: %s, S/W: %s".format(version.lhHwRev.formatWithDots(), version.lhHwRev.formatWithDots()), + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + ) + } + + HorizontalDivider( + modifier = Modifier.padding(start = 16.px.dp, end=16.px.dp), + thickness = 0.1.px.dp, + color = Color.Gray + ) + + // Power Supply + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.px.dp) + .padding(start = 15.px.dp, end = 15.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Power Supply", + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.weight(1f)) + Text( + text = "H/W: %s, S/W: %s".format(version.psHwVer.formatWithDots(), version.psSwVer.formatWithDots()), + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + ) + } + } + + // close button + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 24.px.dp, end = 12.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + modifier = Modifier + .size(92.px.dp, 88.px.dp), + onClick = onClick + ) { + Text( + text = stringResource(R.string.close), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewProgramVersionView() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + ProgramVersionPopup() + } + +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/HomeScreen.kt new file mode 100644 index 0000000..efca84f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/HomeScreen.kt @@ -0,0 +1,532 @@ +package com.laseroptek.raman.ui.screens.home + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.const.LaserParameter +import com.laseroptek.raman.const.LaserStatusType +import com.laseroptek.raman.const.MAX_LASER_COUNT +import com.laseroptek.raman.const.PresetButtonType +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.home.count.HpCountView +import com.laseroptek.raman.ui.screens.home.count.HpView +import com.laseroptek.raman.ui.screens.home.count.LaserCountView +import com.laseroptek.raman.ui.screens.home.dcd.DcdView +import com.laseroptek.raman.ui.screens.home.preset.PresetButton +import com.laseroptek.raman.ui.screens.home.preset.PresetIconButton +import com.laseroptek.raman.ui.screens.home.slider.LaserControlView +import com.laseroptek.raman.ui.screens.home.standby.StandByButton +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@SuppressLint("NewApi") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + paddingValues: PaddingValues = PaddingValues(), + mainViewModel: MainViewModel = hiltViewModel(), +) { + val scope = rememberCoroutineScope() + + val focusManager = LocalFocusManager.current + val context = LocalContext.current + + val pulseAngle by mainViewModel.pulseAngle.collectAsState() + val fluenceAngle by mainViewModel.fluenceAngle.collectAsState() + val repetitionAngle by mainViewModel.repetitionAngle.collectAsState() + + val fluenceList by mainViewModel.fluenceList.collectAsState() + val pulseType by mainViewModel.pulseType.collectAsState() + val repetitionList by mainViewModel.repetitionList.collectAsState() + + // Used to disable the ripple effect on the background click + val interactionSource = remember { MutableInteractionSource() } + val laserStatus by mainViewModel.laserStatus.collectAsState() + val handPiece by mainViewModel.handPiece.collectAsState() + + val laserCount by mainViewModel.laserCount.collectAsState() + val lampCount by mainViewModel.lampCount.collectAsState() + val hpCount by mainViewModel.hpCount.collectAsState() + val lifeTime by mainViewModel.lifeTime.collectAsState() + + val hpCountForMainScreen by remember(hpCount, lifeTime, handPiece) { + mutableStateOf(mainViewModel.getHPCountForMainScreen()) + } + + val presetList by mainViewModel.presetList.collectAsState() + + LaunchedEffect(Unit) { + Timber.d("LaunchedEffect - HomeScreen") + focusManager.clearFocus(force = true) // Hide the keyboard + + Timber.d("Attempted to hide keyboard on EngineerScreen launch") + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + ) { + /////////////////////////////////////////////////////////////////////////////////////////// + // Home Screen Main + + Column( + modifier = Modifier + .fillMaxSize() + .background(color = Color.Transparent) + /* .clickable( + interactionSource = interactionSource, + indication = null // Disable ripple effect + ) + */ + /* + .detectBackgroundTap { + Timber.d("HomeScreen background clicked") + if (laserStatus.laserStatus != 0 && laserStatus.laserStatus != 0x53) { + focusManager.clearFocus() + + scope.launch { + mainViewModel.playBeepSound() + } + + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.LASER_STATUS, + byteArrayOf(LASER_STATUS.STAND_BY.toByte()) + ) + } + + } + */ + .padding(paddingValues) + .padding( + start = 30.px.dp, + end = 30.px.dp, + top = 30.px.dp, + bottom = 30.px.dp + ), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // top + Row( + modifier = Modifier + .fillMaxWidth() // Make the Row take up the full width + .height(50.px.dp) + .background(Color.Transparent), + horizontalArrangement = Arrangement.Center, // Push content to the right + verticalAlignment = Alignment.Top + ) { + HpView( + handPiece = handPiece, + onClick = {} + ) + + Spacer(Modifier.weight(1f)) + + HpCountView( + hpCountForMainScreen, + onClick = {} + ) + + Spacer(Modifier.width(20.px.dp)) + + LaserCountView( + mainViewModel = mainViewModel, + onClick = { + /* + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@LaserCountView + } + */ + + mainViewModel.showResetLaserCountPopup = true + } + ) + } + + // middle + Row( + modifier = Modifier + .fillMaxWidth() + .height(420.px.dp) + .background(Color.Transparent) + .padding(start = 100.px.dp) + , verticalAlignment = Alignment.Bottom + , horizontalArrangement = Arrangement.Center + ) { + // 1. PULSE DURATION + LaserControlView( + modifier = Modifier + .align(Alignment.Bottom) + .size(330.px.dp, 380.px.dp), + //mainViewModel = mainViewModel, + fluenceList = fluenceList, + repetitionList = repetitionList, + pulseType = pulseType, + handPieceType = handPiece.type, + type = LaserStatusType.PULSE_DURATION, + angle = pulseAngle, + onChange = { angle -> + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@LaserControlView + } + + Timber.d("A: onChangePulseDuration - 터치 드래그로 값 변경 / 하단 ▲▼ 로 값 변경 : ${angle}") + mainViewModel.onChangePulseDuration(angle) + }, + onClick = { state -> + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@LaserControlView + } + + //mainViewModel.onClickPulseDuration(state) + mainViewModel.onUpDownButtonClicked(LaserParameter.PulseWidth, state) + } + ) + + Spacer(Modifier.weight(1f)) + + // 2. FLUENCE + LaserControlView( + modifier = Modifier + .size(370.px.dp, 420.px.dp), + //mainViewModel = mainViewModel, + fluenceList = fluenceList, + repetitionList = repetitionList, + pulseType = pulseType, + handPieceType = handPiece.type, + type = LaserStatusType.FLUENCE, + angle = fluenceAngle, + onChange = { angle -> + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@LaserControlView + } + + mainViewModel.onChangeFluence(angle) + }, + onClick = { state -> + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@LaserControlView + } + + //mainViewModel.onClickFluence(state) + mainViewModel.onUpDownButtonClicked(LaserParameter.Fluence, state) + } + ) + + Spacer(Modifier.weight(1f)) + + // 3. REPETITION - SKIN TEMP + LaserControlView( + modifier = Modifier + .size(330.px.dp, 380.px.dp), + fluenceList = fluenceList, + repetitionList = repetitionList, + pulseType = pulseType, + handPieceType = handPiece.type, + type = LaserStatusType.REPETITION, + angle = if (pulseType == 0 || repetitionList.size < 2) 0f else repetitionAngle, + onChange = { angle -> + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@LaserControlView + } + + if (pulseType == 0 || repetitionList.size < 2) { + //Toast.makeText(context, "Single pulse type (0 Hz)", Toast.LENGTH_SHORT).show() + } else { + mainViewModel.onChangeRepetition(angle) + } + + }, + onClick = { state -> + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@LaserControlView + } + + if (pulseType == 0 || repetitionList.size < 2) { + //Toast.makeText(context, "Single pulse type (0 Hz)", Toast.LENGTH_SHORT).show() + } else { + //mainViewModel.onClickRepetition(state) + mainViewModel.onUpDownButtonClicked(LaserParameter.Repetition, state) + } + } + ) + } + + Spacer(modifier = Modifier.height(40.px.dp)) + + // bottom + Row( + modifier = Modifier + .height(100.px.dp) + .fillMaxWidth() + .background(color = Color.Transparent), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // DCD Cooler + DcdView( + mainViewModel = mainViewModel, + onClick = { + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@DcdView + } + + mainViewModel.showDcdSettingPopup = true + } + ) + + Spacer(Modifier.weight(1f)) + + // Preset Load + PresetIconButton( + modifier = Modifier.size(50.px.dp), + type = PresetButtonType.LOAD, + selected = mainViewModel.showPresetLoadPopup, + onClick = { + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@PresetIconButton + } + + // reset selected preset index + //mainViewModel.setSelectedPresetIndex(0) + + Timber.d("onClick - Preset Load") + mainViewModel.showPresetLoadPopup = true + } + ) + + Spacer(Modifier.width(25.px.dp)) + + // Preset Buttons + for (i in 1..5) { + val selectedPresetIndex by mainViewModel.selectedPresetIndex.collectAsState() + PresetButton( + modifier = Modifier.size(50.px.dp), + fontSize = 28, + no = i, + selected = selectedPresetIndex, + //enabled = presetList.filter{ it.priority == i }.size > 0, + onLongClick = { no -> + Timber.d("onClick - Preset Button ${no}") + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@PresetButton + } + + val preset = presetList.firstOrNull { it.handPieceType == handPiece.type && it.priority == no } + if (preset == null || preset.priority < 1) { + Toast.makeText( + context, + "This number is not defined", + Toast.LENGTH_SHORT + ).show() + return@PresetButton + } + + mainViewModel.setSelectedPresetIndex( no ) + mainViewModel.applyPreset(no) + } + ) + Spacer(Modifier.width(25.px.dp)) + } + + // Preset Save + PresetIconButton( + modifier = Modifier.size(50.px.dp), + type = PresetButtonType.SAVE, + selected = mainViewModel.showPresetSavePopup, + onClick = { + Timber.d("onClick - showPresetSavePopup") + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@PresetIconButton + } + + // reset selected preset index + // mainViewModel.setSelectedPresetIndex(0) + + mainViewModel.showPresetSavePopup = true + } + ) + + Spacer(Modifier.weight(1f)) + + // StandBy (Button) + StandByButton( + laserStatus = laserStatus.laserStatus, + onClick = { + Timber.d("WRITE request > LASER_STATUS (Ready: 0x52) Request") + if (handPiece.type == 0) { + Toast.makeText( + context, + "Handpiece is not recognized", + Toast.LENGTH_SHORT + ).show() + return@StandByButton + } + + if (laserCount >= MAX_LASER_COUNT) { + Toast.makeText( + context, + "Max Laser Count reached", + Toast.LENGTH_SHORT + ).show() + return@StandByButton + } + + if (lampCount >= MAX_LASER_COUNT) { + Toast.makeText( + context, + "Max Lamp Count reached", + Toast.LENGTH_SHORT + ).show() + return@StandByButton + } + + val hpCount = mainViewModel.getHPCount() + if (hpCount < 1) { + Toast.makeText( + context, + "Max H/P Count reached", + Toast.LENGTH_SHORT + ).show() + return@StandByButton + } + + mainViewModel.txLaserStatusEntry(0x52) // send ready + + // StandBy Popup + mainViewModel.showStandByPopup = true + } + ) + + Spacer(Modifier.width(10.px.dp)) + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Popups --> move to MainView + + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun HomeScreenPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + mainViewModel.showEnergyDetectPopup = false + HomeScreen( + mainViewModel = mainViewModel + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/HpCountView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/HpCountView.kt new file mode 100644 index 0000000..454bc96 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/HpCountView.kt @@ -0,0 +1,116 @@ +package com.laseroptek.raman.ui.screens.home.count + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.button.RoundTextButton +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.px + +@Composable +fun HpCountView( + hpCountForMainScreen: String = "", + onClick: () -> Unit = {}, +) { + Box( + modifier = Modifier + .size(190.px.dp, 120.px.dp) + .padding(start = 0.px.dp) + .background(Color.Transparent) + ) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 24.px.dp, bottomStart = 24.px.dp)) + .background(Color.Black) + .size(170.px.dp, 40.px.dp) + .align(Alignment.CenterStart) + .padding(start = 20.px.dp) + .zIndex(0f), + ) { + Text( + text = hpCountForMainScreen, + modifier = Modifier + .width(115.px.dp) + .align(alignment = Alignment.CenterVertically), + style = RobotoTypography.titleMedium.copy( + textAlign = TextAlign.End, + fontSize = 22.px.sp, + fontWeight = FontWeight.Medium, + //lineHeight = 23.px.sp, + ), + color = Color.White + ) + //Spacer(Modifier.width(5.px.dp)) + /* + Text( + text = "K", + modifier = Modifier + .width(24.px.dp) + .align(alignment = Alignment.CenterVertically), + style = RobotoTypography.titleSmall.copy( + textAlign = TextAlign.Start, + fontSize = 22.px.sp, + fontWeight = FontWeight.Thin, + letterSpacing = 0.2.px.sp + ), + color = Color.White, + ) + */ + Spacer(Modifier.width(50.px.dp)) + } + + RoundTextButton( + onClick = onClick, + text = "H/P", + contentDescription = "Action description", + buttonSize = 50.px.dp, + modifier = Modifier.align(Alignment.CenterEnd) + ) + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewHpCountView( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + HpCountView() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/HpView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/HpView.kt new file mode 100644 index 0000000..d76ca36 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/HpView.kt @@ -0,0 +1,88 @@ +package com.laseroptek.raman.ui.screens.home.count + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.const.handPieceTypes +import com.laseroptek.raman.data.model.serial.HandPiece +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px + +@Composable +fun HpView( + //mainViewModel: MainViewModel = hiltViewModel(), + handPiece: HandPiece = HandPiece(), + onClick: () -> Unit = {}, +) { + val handPieceTitle = handPieceTypes.get(handPiece.type) + + Box( + modifier = Modifier + .size(175.px.dp, 120.px.dp) + .padding(start = 0.px.dp) + .background(Color.Transparent), + contentAlignment = Alignment.CenterStart + ) { + Row( + modifier = Modifier + .size(120.px.dp, 45.px.dp) + .clip(RoundedCornerShape(45.px.dp)) + .background( + Color(45, 48, 53), + shape = RoundedCornerShape(45.px.dp) + ) + .border( + width = 3.px.dp, + color = Color(236, 197, 66), + shape = RoundedCornerShape(45.px.dp) + ) + .padding(start = 10.px.dp, end = 10.px.dp, top = 6.px.dp, bottom = 6.px.dp) + , + horizontalArrangement = Arrangement.Center, // Push content to the right + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = handPieceTitle, + style = RobotoTypography.bodySmall, + fontSize = 24.px.sp, + lineHeight = 26.px.sp, + letterSpacing = 0.92.px.sp, + fontWeight = FontWeight.Thin, + color = Color(255,255,255), + textAlign = TextAlign.Center + ) + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + //device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewHpView() { + HpView( + handPiece = HandPiece(), + onClick = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/LaserCountView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/LaserCountView.kt new file mode 100644 index 0000000..810f9f2 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/LaserCountView.kt @@ -0,0 +1,128 @@ +package com.laseroptek.raman.ui.screens.home.count + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.button.RoundImageButton +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.px + +@Composable +fun LaserCountView( + mainViewModel: MainViewModel = hiltViewModel(), + onClick: () -> Unit = {}, +) { + val laserCount by mainViewModel.laserCount.collectAsState() + val shotText = "%6d".format(laserCount) + + Box( + modifier = Modifier + .size(230.px.dp, 120.px.dp) + .padding(start = 0.px.dp) + .background(Color.Transparent) + ) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 24.px.dp, bottomStart = 24.px.dp)) + .background(Color.Black) + .size(210.px.dp, 40.px.dp) + .align(Alignment.Center) + .padding(start = 10.px.dp) + .zIndex(0f), + ) { + Text( + text = shotText, + modifier = Modifier + .width(120.px.dp) + .align(alignment = Alignment.CenterVertically), + style = RobotoTypography.bodySmall.copy( + textAlign = TextAlign.End, + fontSize = 22.px.sp, + fontWeight = FontWeight.Thin, + letterSpacing = 0.2.px.sp + ), + color = Color.White + ) + Spacer(Modifier.width(5.px.dp)) + Text( + text = stringResource(id = R.string.shots), + modifier = Modifier + .width(35.px.dp) + .align(alignment = Alignment.CenterVertically), + style = RobotoTypography.bodySmall.copy( + textAlign = TextAlign.Start, + fontSize = 12.px.sp, + fontWeight = FontWeight.Thin, + letterSpacing = 0.2.px.sp + ), + color = Color.White, + ) + Spacer(Modifier.width(50.px.dp)) + } + + RoundImageButton( + onClick = onClick, + painter = painterResource(id = R.drawable.ic_reload), // Replace with your drawable + contentDescription = "Action description", + buttonSize = 50.px.dp, + imageSize = 24.px.dp, + modifier = Modifier.align(Alignment.CenterEnd) + ) + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewLaserCountView( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + mainViewModel.setLaserCount(999999999) + + LaserCountView( + mainViewModel = mainViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/ResetLaserCountPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/ResetLaserCountPopup.kt new file mode 100644 index 0000000..d464d6f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/count/ResetLaserCountPopup.kt @@ -0,0 +1,144 @@ +package com.laseroptek.raman.ui.screens.home.count + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun ResetLaserCountPopup( + //showResetLaserCountPopup: MutableState = mutableStateOf(false), + title: String = stringResource(R.string.reset_laser_count_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(top = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewResetLaserCountPopup() { + remember { mutableStateOf(true) } + ResetLaserCountPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/CoolerButton.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/CoolerButton.kt new file mode 100644 index 0000000..ca054c2 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/CoolerButton.kt @@ -0,0 +1,103 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + + +@Composable +fun CoolerButton( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(50.px.dp)) + .noRippleClickable { onClick() } + .size(26.px.dp) + .border( + border = BorderStroke( + width = 1.px.dp, + brush = SolidColor(Color(189, 189, 189)) + ), + shape = RoundedCornerShape(50.dp) + ) + .dropShadow( + shape = RoundedCornerShape(50.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.06f), + blur = 3.px.dp, + offsetY = 1.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .background( + Brush.linearGradient( + colors = listOf( + Color(192,192,192), + Color(246,246,246), + ) + ), + shape = RoundedCornerShape(50.px.dp) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_right_2), + contentDescription = null, + modifier = Modifier + .size(16.px.dp), + ) + } + +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun CoolerButtonPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + CoolerButton( + //mainViewModel = mainViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/CoolerTitle.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/CoolerTitle.kt new file mode 100644 index 0000000..b7053b5 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/CoolerTitle.kt @@ -0,0 +1,107 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.px + + +@Composable +fun CoolerTitle( + title: String = "", + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(7.px.dp)) + .size(41.2.px.dp, 20.px.dp) + .border( + border = BorderStroke( + width = 1.px.dp, + brush = SolidColor(Color(228, 228, 231)) + ), + shape = RoundedCornerShape(7.px.dp) + ) + .background( + Brush.linearGradient( + colors = listOf( + Color(237,237,237), + Color(255,255,255), + ) + ), + shape = RoundedCornerShape(7.px.dp) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_cooler), + contentDescription = null, + modifier = Modifier + .size(16.px.dp), + ) + Spacer(modifier = Modifier.width(2.px.dp)) + Text( + text = title, + style = RobotoTypography.bodySmall, + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + color = Color(30,58,138) + ) + } +} + + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun CoolerTitlePreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + CoolerTitle( + //mainViewModel = mainViewModel + title = "COOLER TITLE" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdBox.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdBox.kt new file mode 100644 index 0000000..567ca60 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdBox.kt @@ -0,0 +1,184 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.serial.SprayDcd +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + +@Composable +fun DcdBox( + modifier: Modifier = Modifier, + id: Int = 0, + selNumber: Int = 0, + sprayDcdList: MutableList = mutableStateListOf(), + onClickDcd: (Int) -> Unit = {}, + onClickOption: (Int) -> Unit = {} +) { + Row( + modifier = Modifier + .noRippleClickable { onClickDcd.invoke(id) } + .size(221.px.dp, 64.px.dp) + .clip(RoundedCornerShape(12.px.dp)) + .background( + brush = + if (selNumber == id) + Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color(255, 255, 254), + 0.5f to Color(255, 253, 248), + 1.0f to Color(254, 245, 216) + ) + ) + else + Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color(255, 255, 255), + 1.0f to Color(240, 240, 240) + ) + ) + ) + .border( + width = 1.px.dp, + color = if (selNumber == id) Color(222, 173, 11) else Color(221, 221, 221), + shape = RoundedCornerShape(12.px.dp) + ) + .padding(start = 12.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Left + Column( + modifier = Modifier + .fillMaxHeight() + .width(160.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // upper + Row( + modifier = Modifier + .fillMaxWidth() + .height(20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + CoolerTitle(id.toString()) + } + + Spacer(modifier = Modifier.height(1.px.dp)) + + // lower + Row( + modifier = Modifier + .fillMaxWidth() + .height(20.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // spray + Text( + text = if(sprayDcdList.size > id) { + val option = sprayDcdList[id] + "Spray: %d".format( + option.sprayTime.toInt() + ) + } else "Spray: 0", + style = RobotoTypography.labelMedium, + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f) + ) + + // divider + Spacer(modifier = Modifier.width(5.px.dp)) + VerticalDivider( + color = Color(161,161,170), + thickness = 1.px.dp, + modifier = Modifier.size(1.px.dp, 14.px.dp) + ) + Spacer(modifier = Modifier.width(5.px.dp)) + + // delay + Text( + text = if(sprayDcdList.size > id) { + val option = sprayDcdList[id] + "Delay: %d".format( + option.sprayDelay.toInt() + ) + } else "Delay: 0" + , + style = RobotoTypography.labelMedium, + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f) + ) + } + } + + // Right + Column( + modifier = Modifier + .fillMaxHeight() + .width(70.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CoolerButton( onClick = { onClickOption.invoke(id) }) + } + + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + device = "spec:width=1280dp,height=720dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewDcdBox( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + DcdBox ( + //mainViewModel = mainViewModel, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdOptionDialog.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdOptionDialog.kt new file mode 100644 index 0000000..91daabd --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdOptionDialog.kt @@ -0,0 +1,435 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.dcdDelayValues +import com.laseroptek.raman.const.dcdSprayValues +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.serial.SprayDcd +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun DcdOptionDialog( + optionValue: SprayDcd = SprayDcd(), + onClick: (Boolean) -> Unit= {}, + onClickSprayTime: (Int) -> Unit = {}, + onClickSprayDelay: (Int) -> Unit = {} +) { + FullScreen { + Column( + modifier = Modifier + .size(392.px.dp, 184.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(end = 10.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Upper + Row( + modifier = Modifier + .fillMaxWidth() + .height(96.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp, top = 20.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Spray + Column( + modifier = Modifier + .size(148.px.dp, 72.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // title + Text( + text = stringResource(R.string.spray_title), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color(73,69,79), + textAlign = TextAlign.Start + ) + + Spacer(modifier = Modifier.weight(1f)) + + // values + Row( + modifier = Modifier + .size(148.px.dp, 36.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // < + Row( + modifier = Modifier + .noRippleClickable { + val idx = dcdSprayValues.indexOf( optionValue.sprayTime.toString() ) + Timber.d("idx : ${idx}") + if (idx > 0) { + //optionValue.sprayTime = dcdSprayValues[idx - 1].toInt() + /* + optionValue = optionValue.copy( + sprayTime = dcdSprayValues[idx - 1].toInt() + ) + */ + val sprayTime = dcdSprayValues[idx - 1].toInt() + onClickSprayTime(sprayTime) + } + Timber.d("onClickSprayTime Down -> optionValue : ${optionValue}") + } + .size(28.px.dp) + .clip(RoundedCornerShape(8.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ) + .border( + width = 1.4.px.dp, + color = Color(222,173,11), + shape = RoundedCornerShape(8.px.dp) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_left_yellow), + contentDescription = null, + modifier = Modifier.size(16.px.dp), + contentScale = ContentScale.Crop + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + // Text + Text( + text = "%d".format(optionValue.sprayTime), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 36.px.sp, + color = Color.Black, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.weight(1f)) + + // > + Row( + modifier = Modifier + .noRippleClickable { + val idx = dcdSprayValues.indexOf( optionValue.sprayTime.toString() ) + Timber.d("idx : ${idx}") + if (idx < dcdSprayValues.size - 1) { + //optionValue.sprayTime = dcdSprayValues[idx + 1].toInt() + /* + optionValue = optionValue.copy( + sprayTime = dcdSprayValues[idx + 1].toInt() + ) + */ + val sprayTime = dcdSprayValues[idx + 1].toInt() + onClickSprayTime(sprayTime) + Timber.d("onClickSprayTime UP -> optionValue : ${optionValue}") + } + } + .size(28.px.dp) + .clip(RoundedCornerShape(8.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ) + .border( + width = 1.4.px.dp, + color = Color(222,173,11), + shape = RoundedCornerShape(8.px.dp) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_right_yellow), + contentDescription = null, + modifier = Modifier.size(16.px.dp), + contentScale = ContentScale.Crop + ) + } + } + } + + // Divider + Spacer(Modifier.width(25.px.dp)) + VerticalDivider( + modifier = Modifier.size(1.px.dp, 72.px.dp), + thickness = 1.px.dp, + color = Color(209,209,209) + ) + Spacer(Modifier.width(25.px.dp)) + + // Delay + Column( + modifier = Modifier + .size(148.px.dp, 72.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // title + Text( + text = stringResource(R.string.delay_title), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color(73,69,79), + textAlign = TextAlign.Start + ) + + Spacer(modifier = Modifier.weight(1f)) + + // values + Row( + modifier = Modifier + .size(148.px.dp, 36.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // < + Row( + modifier = Modifier + .noRippleClickable { + val idx = dcdDelayValues.indexOf( optionValue.sprayDelay.toString() ) + Timber.d("idx : ${idx}") + if (idx > 0) { + //optionValue.sprayDelay = dcdDelayValues[idx - 1].toInt() + /* + optionValue = optionValue.copy( + sprayDelay = dcdDelayValues[idx - 1].toInt() + ) + */ + val sprayDelay = dcdDelayValues[idx - 1].toInt() + onClickSprayDelay(sprayDelay) + Timber.d("onClickSprayDelay Down -> optionValue : ${optionValue}") + } + } + .size(28.px.dp) + .clip(RoundedCornerShape(8.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ) + .border( + width = 1.4.px.dp, + color = Color(222,173,11), + shape = RoundedCornerShape(8.px.dp) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_left_yellow), + contentDescription = null, + modifier = Modifier.size(16.px.dp), + contentScale = ContentScale.Crop + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + // Text + Text( + text = "%d".format(optionValue.sprayDelay), + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 36.px.sp, + color = Color.Black, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.weight(1f)) + + // > + Row( + modifier = Modifier + .noRippleClickable { + val idx = dcdDelayValues.indexOf( optionValue.sprayDelay.toString() ) + Timber.d("idx : ${idx}") + if (idx < dcdDelayValues.size - 1) { + //optionValue.sprayDelay = dcdDelayValues[idx + 1].toInt() + /* + optionValue = optionValue.copy( + sprayDelay = dcdDelayValues[idx + 1].toInt() + ) + */ + val sprayDelay = dcdDelayValues[idx + 1].toInt() + onClickSprayDelay(sprayDelay) + Timber.d("onClickSprayDelay Up -> optionValue : ${optionValue}") + } + } + .size(28.px.dp) + .clip(RoundedCornerShape(8.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ) + .border( + width = 1.4.px.dp, + color = Color(222,173,11), + shape = RoundedCornerShape(8.px.dp) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_right_yellow), + contentDescription = null, + modifier = Modifier.size(16.px.dp), + contentScale = ContentScale.Crop + ) + } + } + } + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(66.px.dp) + .padding(end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(55.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick.invoke(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewDcdOptionDialog( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + DcdOptionDialog( + //mainViewModel = mainViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdSettingPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdSettingPopup.kt new file mode 100644 index 0000000..359e05f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdSettingPopup.kt @@ -0,0 +1,670 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.laseroptek.raman.R +import com.laseroptek.raman.const.CMD +import com.laseroptek.raman.const.PURGE_STATUS +import com.laseroptek.raman.const.READ_WRITE +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.serial.DcdGasOnOff +import com.laseroptek.raman.data.model.serial.PurgeBubble +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.button.ButtonWithGradientHIconAndText +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.launch +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DcdSettingPopup( + mainViewModel: MainViewModel = hiltViewModel(), + //dcdViewModel: DcdViewModel = hiltViewModel(), + title: String = stringResource(R.string.dcd_setting_title), + onDismiss: () -> Unit = {}, +) { + val scope = rememberCoroutineScope() + + var showDcdOptionDialog by remember { mutableStateOf(false) } + var showResetDcdCountDialog by remember { mutableStateOf(false) } + var showPurgeProgressDialog by remember { mutableStateOf(false) } + + val gasChargeRate by mainViewModel.gasChargeRate.collectAsStateWithLifecycle() + + val selectedSprayDcdIndex by mainViewModel.selectedSprayDcdIndex.collectAsState() + //val selectedOptionIndex by mainViewModel.selectedOptionIndex.collectAsState() + + Timber.d("Gas Charge: ${gasChargeRate.toInt()}%") + + //val optionValue = remember { mutableStateOf(mainViewModel.sprayDcdList[selectedSprayDcdIndex].copy()) } + val sprayDcd by mainViewModel.sprayDcd.collectAsState() + + val dcdCount by mainViewModel.dcdCount.collectAsState() + val dcdType by mainViewModel.dcdType.collectAsState() + + // 0: initial, 0x41(A: Accept), 0x42(B: Complete), 0x46(F: Fail/Refused) + val purgeBubble by mainViewModel.purgeBubble.collectAsState() + + LaunchedEffect(Unit) { + // init purgeBubble variable + mainViewModel.setPurgeBubble( + PurgeBubble(PURGE_STATUS.INIT) + ) + + //dcdViewModel.setSprayDcdList(mainViewModel.sprayDcdList) + //dcdViewModel.setSelectedSprayDcdIndex(mainViewModel.selectedSprayDcdIndex.value) + + //Timber.d("sprayDcdList: ${dcdViewModel.sprayDcdList}") + //Timber.d("selectedSprayDcdIndex: ${selectedSprayDcdIndex}") + } + + FullScreen(contentAlignment= Alignment.Center) { + Box { + // Main Popup Content + Column( + modifier = Modifier + .size(574.px.dp, 372.px.dp) + .clip(RoundedCornerShape(16.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(topStart = 16.px.dp, bottomStart = 16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 2.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(topStart = 16.px.dp, bottomStart = 16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + //.padding(start = 0.px.dp, end = 0.px.dp, top = 0.px.dp, bottom = 0.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // Close + Row( + modifier = Modifier + .fillMaxWidth() + .height(30.px.dp) + .padding(top = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.Bottom, + ) { + // Close + IconButton( + onClick = { + // onClickSave.invoke(false) + Timber.d("onDismiss") + + //mainViewModel.setSelectedSprayDcdIndex( selectedSprayDcdIndex ) + //mainViewModel.setSprayDcdList( dcdViewModel.sprayDcdList ) + + //Timber.d("selectedSprayDcdIndex: ${selectedSprayDcdIndex}") + //Timber.d("sprayDcdList: ${dcdViewModel.sprayDcdList}") + + //mainViewModel.saveSprayDcdListToPreference() + + onDismiss.invoke() + }, + modifier = Modifier.size(32.px.dp, 32.px.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, + modifier = Modifier.size(20.px.dp), + contentScale = ContentScale.Crop + ) + } + } + + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(35.px.dp) + .padding(start = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Bottom + ) { + // Cooler Image + Image( + painter = painterResource(id = R.drawable.ic_cooler_2x), + contentDescription = null, + modifier = Modifier + .size(35.px.dp), + ) + + Spacer(modifier = Modifier.width(2.px.dp)) + + // Title + Text( + text = title, + style = RobotoTypography.headlineSmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 24.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Expected Value + Row( + modifier = Modifier + .fillMaxWidth() + .height(20.px.dp) + .padding(end = 30.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // DCD Count / Expected Value + Text( + text = "%06d".format(dcdCount), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 10.px.sp, + color = Color(0,0,0), + ) + Text( + text = "/%06d".format( + (if (dcdType.canType == 700) 1 else 2) * (6000 * dcdType.lifeSpan).toInt() + ), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 10.px.sp, + color = Color(82,82,91), + ) + } + + // { Spray & Delay } & Battery + Row( + modifier = Modifier + .fillMaxWidth() + .height(210.px.dp) + .padding(start = 20.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top + ) { + // Left { Spray & Delay } + Column( + modifier = Modifier + .fillMaxHeight() + .width(221.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // 1, 3, 5 + for (i in 1..5 step 2) { + DcdBox( + id = i, + selNumber = selectedSprayDcdIndex, + sprayDcdList = mainViewModel.sprayDcdList, + onClickDcd = { + mainViewModel.setSelectedSprayDcdIndex(i) + val optionValue = mainViewModel.sprayDcdList[i] + mainViewModel.setSprayDcd(optionValue) + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, optionValue) + + scope.launch { + mainViewModel.saveSprayDcdIndexToPreference() + mainViewModel.saveSprayDcdToPreference() + } + + }, + onClickOption = { + mainViewModel.setSelectedSprayDcdIndex(i) + val optionValue = mainViewModel.sprayDcdList[i] + mainViewModel.setSprayDcd(optionValue) + + scope.launch { + mainViewModel.saveSprayDcdIndexToPreference() + mainViewModel.saveSprayDcdToPreference() + } + + showDcdOptionDialog = true + } + ) + if (i < 5) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + // Right { Spray & Delay } + Column( + modifier = Modifier + .fillMaxHeight() + .width(221.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // 2, 4, 6 + for (i in 2..6 step 2) { + DcdBox( + id = i, + selNumber = selectedSprayDcdIndex, + sprayDcdList = mainViewModel.sprayDcdList, + onClickDcd = { + mainViewModel.setSelectedSprayDcdIndex(i) + val optionValue = mainViewModel.sprayDcdList[i] + mainViewModel.setSprayDcd(optionValue) + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, optionValue) + + scope.launch { + mainViewModel.saveSprayDcdToPreference() + } + }, + onClickOption = { + mainViewModel.setSelectedSprayDcdIndex(i) + val optionValue = mainViewModel.sprayDcdList[i] + mainViewModel.setSprayDcd(optionValue) + + scope.launch { + mainViewModel.saveSprayDcdToPreference() + } + + showDcdOptionDialog = true + } + ) + if (i < 6) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + + Spacer(modifier = Modifier.width(10.px.dp)) + + // Battery (Percent) + GradientSlider( + modifier = Modifier + .fillMaxHeight() + .size(40.px.dp, 210.px.dp), + chargeRate = gasChargeRate.toInt() + ) + + // Icon + Column( + modifier = Modifier + .fillMaxHeight() + .width(50.px.dp) + .padding(start = 3.px.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_full), + contentDescription = "Button Icon", + modifier = Modifier.size(30.px.dp, 30.px.dp), + contentScale = ContentScale.Crop + + ) + Spacer(modifier = Modifier.weight(1f)) // Add space between icon and text + Image( + painter = painterResource(id = R.drawable.ic_empty), + contentDescription = "Button Icon", + modifier = Modifier.size(30.px.dp, 30.px.dp), + contentScale = ContentScale.Crop + ) + } + } + + Spacer(modifier = Modifier.height(20.px.dp)) + + // { Valve - Purge - Spray } icons + Row(modifier = Modifier + .fillMaxWidth() + .height(80.px.dp) + .padding(start = 20.px.dp, end = 30.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top + ) { + // Purge Button + // purgeBubble - 0: initial, 0x41(A: Accept), 0x42(B: Complete), 0x46(F: Fail/Refused) + ButtonWithGradientHIconAndText( + modifier = Modifier + .size(97.px.dp, 40.px.dp) + .clip(RoundedCornerShape(20.px.dp)) + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(234, 232, 235), + 1.0f to Color(255, 251, 255), + ) + ), + shape = RoundedCornerShape(20.px.dp) + ) + .border( + 8.px.dp, + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(250, 250, 250), + 1.0f to Color(233, 231, 234), + ) + ), + shape = RoundedCornerShape(20.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(20.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.09f), + blur = 12.px.dp, + offsetX = 0.px.dp, + offsetY = 4.px.dp, + spread = 0.px.dp + ), + title = "Purge", + painter = painterResource(id = R.drawable.ic_purge), + onClick = { + Timber.d("onClickPurge") + + if (purgeBubble.value == PURGE_STATUS.ACCEPT) { + Timber.d("onClickPurge: PURGE_STATUS.ACTIVATE") + return@ButtonWithGradientHIconAndText + } + + // init purgeBubble variable + mainViewModel.setPurgeBubble( + PurgeBubble(PURGE_STATUS.INIT) + ) + + // send tx + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.PURGE_BUBBLE, + byteArrayOf(PURGE_STATUS.ACCEPT.toByte()) + ) + + showPurgeProgressDialog = true + } + ) + + Spacer(modifier = Modifier.weight(1f)) + + + // Refresh Button + //if (chargeRate < 20f) { + ButtonWithGradientHIconAndText( + modifier = Modifier + .size(97.px.dp, 40.px.dp) + .clip(RoundedCornerShape(20.px.dp)) + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(234, 232, 235), + 1.0f to Color(255, 251, 255), + ) + ), + shape = RoundedCornerShape(20.px.dp) + ) + .border( + 8.px.dp, + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(250, 250, 250), + 1.0f to Color(233, 231, 234), + ) + ), + shape = RoundedCornerShape(20.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(20.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.09f), + blur = 12.px.dp, + offsetX = 0.px.dp, + offsetY = 4.px.dp, + spread = 0.px.dp + ), + title = "Refresh", + painter = painterResource(id = R.drawable.ic_refresh), + onClick = { + Timber.d("onClickRefresh") + // Perform action when button is clicked + // onClickPurge.invoke(!purgeStatus.value) + + /* + Timber.d("onClickRefresh") + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.PURGE_BUBBLE, + byteArrayOf(PURGE_STATUS.ACTIVATE.toByte()) + ) + */ + + // mainViewModel.setDcdCount(0) + // mainViewModel.setCanReplaced( false ) + + showResetDcdCountDialog = true + } + ) + //} + + // end if (canReplaced) + + Spacer(modifier = Modifier.width(20.px.dp)) + + // Test Button + ButtonWithGradientHIconAndText( + modifier = Modifier + .size(97.px.dp, 40.px.dp) + .clip(RoundedCornerShape(20.px.dp)) + .background( + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(234, 232, 235), + 1.0f to Color(255, 251, 255), + ) + ), + shape = RoundedCornerShape(20.px.dp) + ) + .border( + 8.px.dp, + brush = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(250, 250, 250), + 1.0f to Color(233, 231, 234), + ) + ), + shape = RoundedCornerShape(20.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(20.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.09f), + blur = 12.px.dp, + offsetX = 0.px.dp, + offsetY = 4.px.dp, + spread = 0.px.dp + ), + title = "Test", + painter = painterResource(id = R.drawable.ic_spray), + onClick = { + Timber.d("onClickTest") + /* DCD Sparay : 0x07 + val sprayDcd = mainViewModel.sprayDcdList[selectedSprayDcdIndex] + + if (selectedSprayDcdIndex == 0) { + sprayDcd.status = 0x44 // off + } else { + sprayDcd.status = 0x41 // on + } + + Timber.d("idx: ${selectedSprayDcdIndex}, sprayDcd: ${sprayDcd}") + + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, sprayDcd) + + // sprayDCD 수신시 증가 + //mainViewModel.setDcdCount( mainViewModel.dcdCount.value + 1 ) + */ + + val data = DcdGasOnOff(status = 0x54) // Test + mainViewModel.txPacket(READ_WRITE.WRITE, CMD.DCD_GAS, data) + } + ) + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Popup + + if (showDcdOptionDialog) { + DcdOptionDialog( + //showDcdOptionDialog = mainViewModel.showDcdOptionDialog, + optionValue = sprayDcd, //mainViewModel.optionValue, + //title = "Set the Option%d Value".format(mainViewModel.selectedOptionIndex.value), + onClickSprayTime = { sprayTime -> + mainViewModel.setSprayDcd( + sprayDcd.copy( + sprayTime = sprayTime + ) + ) + mainViewModel.setSprayDcd( + selectedSprayDcdIndex, + sprayDcd + ) + + scope.launch { + mainViewModel.saveSprayDcdToPreference() + } + }, + onClickSprayDelay = { sprayDelay -> + mainViewModel.setSprayDcd( + sprayDcd.copy( + sprayDelay = sprayDelay + ) + ) + mainViewModel.setSprayDcd( + selectedSprayDcdIndex, + sprayDcd + ) + + scope.launch { + mainViewModel.saveSprayDcdToPreference() + } + }, + onClick = { ok -> + Timber.d("DcdOptionDialog -> ok: $ok") + if (ok) { + mainViewModel.setSprayDcd( + selectedSprayDcdIndex, + sprayDcd + ) + + mainViewModel.setSelectedSprayDcdIndex( selectedSprayDcdIndex ) + //mainViewModel.setSprayDcdList( mainViewModel.sprayDcdList ) + + scope.launch { + mainViewModel.saveSprayDcdIndexToPreference() + mainViewModel.saveSprayDcdToPreference() + mainViewModel.saveSprayDcdListToPreference() + } + + } + + // save values to Preference + showDcdOptionDialog = false + } + ) + } + + if (showResetDcdCountDialog) { + ResetDcdCountDialog( + title = stringResource(R.string.reset_dcd_count_title), + onClick = { okValue -> + Timber.d("ResetDcdCountDialog: $okValue") + if (okValue) { + mainViewModel.setDcdCount(0) + } + showResetDcdCountDialog = false + } + ) + } + + // Purge Progress + if (showPurgeProgressDialog) { + PurgeProgressDialog( + onDismissRequest = { + showPurgeProgressDialog = false + }, + purgeBubble = purgeBubble + ) + } + + + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + //device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewDcdSettingPopup( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + DcdSettingPopup( + mainViewModel = mainViewModel, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdView.kt new file mode 100644 index 0000000..646bbec --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdView.kt @@ -0,0 +1,210 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun DcdView( + mainViewModel: MainViewModel = hiltViewModel(), + onClick: () -> Unit = {}, +) { + val selectedSprayDcdIndex by mainViewModel.selectedSprayDcdIndex.collectAsState() + val sprayDcd by mainViewModel.sprayDcd.collectAsState() + + ConstraintLayout( + modifier = Modifier + .size(209.px.dp, 74.px.dp) + .background(Color.Transparent) + .offset(x = (0).px.dp) + ) { + val (boxLRef, boxRRef) = createRefs() + + // right + Row( + modifier = Modifier + .border( + width = 2.px.dp, + color = Color(50,50,50), + shape = RoundedCornerShape(999.px.dp) + ) + .size(176.px.dp, 62.px.dp) + .clip( + RoundedCornerShape( + topStart = 0.dp, // No rounding on top-left + topEnd = 999.px.dp, // Full rounding on top-right + bottomStart = 0.px.dp, // No rounding on bottom-left + bottomEnd = 999.px.dp // Full rounding on bottom-right + ) + ) + .background(Color(37,37,36)) + .constrainAs(boxRRef) { + end.linkTo(parent.end) + centerVerticallyTo(parent) + } + , verticalAlignment = Alignment.CenterVertically + , horizontalArrangement = Arrangement.End + ) { + // Spray + Timber.d("selectedSprayDcdIndex: ${selectedSprayDcdIndex}") + //Timber.d("sprayDcdList.size: ${sprayDcdList.size}") + + val sprayTime = "%d".format(sprayDcd.sprayTime) + Timber.d("sprayTime: ${sprayTime}") + + Column( + modifier = Modifier + .width(45.px.dp) + .fillMaxHeight() + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Center + ) { + Text( + text = "Spray", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + Text( + text = sprayTime, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 26.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + } + + Spacer( Modifier.width(10.px.dp)) + + VerticalDivider( + color = Color(161,161,170), + thickness = 1.px.dp, + modifier = Modifier.size(1.px.dp, 30.px.dp) + ) + + Spacer( Modifier.width(10.px.dp)) + + // Delay + Timber.d("selectedSprayDcdIndex.value: ${selectedSprayDcdIndex}") + //Timber.d("sprayDcdList.size: ${sprayDcdList.size}") + + val sprayDelay = "%d".format(sprayDcd.sprayDelay) + Timber.d("sprayDelay: ${sprayDelay}") + + Column( + modifier = Modifier + .width(45.px.dp) + .fillMaxHeight() + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Center + ) { + Text( + text = "Delay", + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + Text( + text = sprayDelay, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Normal, + fontSize = 26.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + } + + Spacer( Modifier.width(25.px.dp)) + } + + // left - DCD setting button + Box( + modifier = Modifier + .noRippleClickable(onClick = { onClick.invoke() }) + .size(74.px.dp) + .background(Color.Transparent) + .constrainAs(boxLRef) { + start.linkTo(parent.start) + centerVerticallyTo(parent) + } + ) { + Image( + painter = painterResource(id = R.drawable.ic_dcd_setting2_btn), + contentDescription = "Preset Setting", + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun DcdCoolerPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + DcdView( + mainViewModel = mainViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdViewModel.kt new file mode 100644 index 0000000..bffaf62 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/DcdViewModel.kt @@ -0,0 +1,43 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.ViewModel +import com.laseroptek.raman.const.SprayDcdList +import com.laseroptek.raman.data.model.serial.SprayDcd +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class DcdViewModel @Inject constructor( +) : ViewModel() { + + // copied from mainViewModel + // sprayDcdList - Spreay DCD Option List (6 Fixed Size List) + var sprayDcdList = SprayDcdList.toMutableStateList() + fun setSprayDcdList(list: List) { + sprayDcdList = list.map{ it.copy() }.toMutableStateList() + } + fun setSprayDcd(idx: Int, item: SprayDcd) { + sprayDcdList[idx] = item.copy() + } + + // copied from mainViewModel + // selectedSprayDcdIndex - 메인 화면 DCD { Spray | Delay } + // - DcdSettingPopup에서 선택한 sprayDcdList index + private val _selectedSprayDcdIndex: MutableStateFlow = MutableStateFlow(0) + val selectedSprayDcdIndex = _selectedSprayDcdIndex.asStateFlow() + fun setSelectedSprayDcdIndex(i: Int) { _selectedSprayDcdIndex.value = i } + + // selectedSprayDcdIndex - 메인 화면 DCD { Spray | Delay } + // - DcdSettingPopup에서 선택한 sprayDcdList index + private val _selectedOptionIndex: MutableStateFlow = MutableStateFlow(0) + val selectedOptionIndex = _selectedOptionIndex.asStateFlow() + fun setSelectedOptionIndex(i: Int) { _selectedOptionIndex.value = i } + + init { + Timber.d("DcdViewModel init") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/GradientSlider.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/GradientSlider.kt new file mode 100644 index 0000000..3250621 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/GradientSlider.kt @@ -0,0 +1,108 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + + +@Composable +fun GradientSlider( + modifier: Modifier = Modifier, + chargeRate: Int = 0, // Value from (0 .. 100) +) { + val chargeIndex = ((chargeRate.coerceIn(0, 100) + 4) / 5).toInt() // 0..19 + Timber.d("chargeRate: $chargeRate, chargeIndex: $chargeIndex") + + Box( + modifier = modifier + .clip(RoundedCornerShape(6.px.dp)) + .border(width = 1.px.dp, color = Color.Black.copy(alpha = 0.3f), shape = RoundedCornerShape(6.px.dp)), + ) { + // Percent + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + for (i in 19 downTo 0) { + val (backgroundColor, borderColor) = if (i > chargeIndex) { + Pair(Color.White, Color.DarkGray) + } else { + if ( i < 3 && chargeIndex < 3) { + Pair(Color.Red, Color.DarkGray) + } else { + Pair(Color.DarkGray, Color.DarkGray) + } + + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(10.px.dp) + ) { + Box( + modifier = Modifier + .size(width = 30.px.dp, height = 7.px.dp) + .border(width = 0.5.px.dp, color = borderColor) + .background(color = backgroundColor) + .align(Alignment.Center) + ) + } + + } + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=portrait" +) +@Composable +fun GradientSliderPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + GradientSlider( + //mainViewModel = mainViewModel + modifier = Modifier + .size(40.px.dp, 210.px.dp), + chargeRate = 20 + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/PurgeProgressDialog.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/PurgeProgressDialog.kt new file mode 100644 index 0000000..7a3e4a3 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/PurgeProgressDialog.kt @@ -0,0 +1,294 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.laseroptek.raman.R +import com.laseroptek.raman.const.PURGE_STATUS +import com.laseroptek.raman.data.model.serial.PurgeBubble +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow + +@Composable +fun PurgeProgressDialog( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + dialogProperties: DialogProperties = DialogProperties(), + purgeBubble: PurgeBubble = PurgeBubble(0) // 0x41 (Accept), 0x42 (Complete), 0x44(Deny), 0x00(Initial) +) { + /* + var showFirstImage by remember { mutableStateOf(true) } + val crossfadeDurationMillis: Int = 50 + val visibleDurationMillis: Long = 500 + val closeIcon: ImageVector = Icons.Filled.Close + + LaunchedEffect(Unit, visibleDurationMillis) { // Keyed to restart if visibleDurationMillis changes + while (purgeBubble.value == 0x41) { + delay(visibleDurationMillis) + showFirstImage = !showFirstImage + } + } + */ + val lottieJsonAssetName: String = "bubble.json" + val iterations: Int = LottieConstants.IterateForever + val speed: Float = 1.0f + val composition by rememberLottieComposition(LottieCompositionSpec.Asset(lottieJsonAssetName)) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = iterations, + speed = speed + ) + + Dialog( + onDismissRequest = onDismissRequest, + properties = dialogProperties + ) { + Column( + modifier = Modifier + .size(280.px.dp, 250.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(color = Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0,0,0).copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0,0,0).copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + , + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.px.dp) + .padding(start = 24.px.dp, top = 24.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Top + ) { + Text( + text = "Purge", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 20.px.sp, + color = Color.Black + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + when (purgeBubble.value) { + PURGE_STATUS.INIT -> { + // requested (init) + Image( + painter = painterResource(id = R.drawable.ic_purge_off), + contentDescription = null, + modifier = Modifier + .size(42.px.dp), + contentScale = ContentScale.Fit + ) + } + PURGE_STATUS.COMPLTE -> { + // completed + Image( + painter = painterResource(id = R.drawable.ic_completed), + contentDescription = null, + modifier = Modifier + .size(42.px.dp), + contentScale = ContentScale.Fit + ) + } + PURGE_STATUS.FAIL -> { + // failed + Image( + painter = painterResource(id = R.drawable.ic_failed), + contentDescription = null, + modifier = Modifier + .size(42.px.dp), + contentScale = ContentScale.Fit + ) + } + else -> { + // 0x41 or initial + Row( + modifier = Modifier + .fillMaxWidth() + .height(100.px.dp) + ) { + LottieAnimation( + composition = composition, + progress = { progress }, // Use a lambda that returns the progress + modifier = modifier, + contentScale = ContentScale.Fit + ) + } + + /* + Crossfade( + targetState = showFirstImage, + animationSpec = tween(durationMillis = crossfadeDurationMillis), + modifier = modifier.size(42.px.dp), + label = "ImageCrossfade" + ) { isFirstImage -> + val painter = + if (isFirstImage) painterResource(id = R.drawable.ic_purge_on) + else painterResource(id = R.drawable.ic_purge_off) + Image( + painter = painter, + contentDescription = null, + modifier = Modifier + .size(42.px.dp), + contentScale = ContentScale.Fit + ) + } + */ + } + } + + Spacer(modifier = Modifier.height(10.px.dp)) + + // Body Text + val bodyText = when (purgeBubble.value) { + PURGE_STATUS.INIT -> "The purge is requested" + PURGE_STATUS.COMPLTE -> "The purge is complete" + PURGE_STATUS.FAIL -> "The operation failed.\nPlease try again." + else -> "In progress" + } + + Text( + text = bodyText, + textAlign = TextAlign.Center, + style = RobotoTypography.bodySmall.copy( + lineHeight = 16.px.sp + ), + fontSize = 15.px.sp, + color = Color.Black, + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Ok + if (purgeBubble.value == PURGE_STATUS.COMPLTE || purgeBubble.value == PURGE_STATUS.FAIL) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = onDismissRequest + ) { + Text( + text = stringResource(R.string.close), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 15.px.sp, + color = Color.Black + ) + } + } + } + } + } +} + +// Preview for the dialog with an image +@Preview(showBackground = true, device = "spec:width=1080px,height=1920px,dpi=420") +@Composable +private fun PurgeProgressDialogPreviewWithImage() { + + val _purgeBubble: MutableStateFlow = MutableStateFlow(PurgeBubble()) + val purgeBubble: MutableStateFlow = _purgeBubble + fun setPurgeBubble(p: PurgeBubble) { + _purgeBubble.value = p + } + + val visibleDurationMillis: Long = 500 + LaunchedEffect(Unit, visibleDurationMillis) { // Keyed to restart if visibleDurationMillis changes + for (i in 0..1000 ) { + delay(visibleDurationMillis) + when (i%4) { + 0 -> setPurgeBubble(PurgeBubble(PURGE_STATUS.INIT)) // Init + 1 -> setPurgeBubble(PurgeBubble(PURGE_STATUS.ACCEPT)) // Accept + 2 -> { + setPurgeBubble(PurgeBubble(PURGE_STATUS.COMPLTE)) + } // Complete + 3 -> { + setPurgeBubble(PurgeBubble(PURGE_STATUS.FAIL)) + } // Fail/Refuse + } + } + } + + MaterialTheme { // Wrap with MaterialTheme for previews to apply colors and typography + // Simulate a screen background for better preview context + Box( + modifier = Modifier + .size(340.px.dp, 250.px.dp) + .background(Color.Black), // Dimmed background like a typical dialog overlay + contentAlignment = Alignment.Center + ) { + val purgeBubble by purgeBubble.collectAsState() + PurgeProgressDialog( + onDismissRequest = { + // Handle dismiss if needed + }, + purgeBubble = purgeBubble + ) + } + + } +} + diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/ResetDcdCountDialog.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/ResetDcdCountDialog.kt new file mode 100644 index 0000000..20e18b6 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/ResetDcdCountDialog.kt @@ -0,0 +1,160 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun ResetDcdCountDialog( + //showResetDcdCountDialog: MutableState = mutableStateOf(false), + title: String = stringResource(R.string.reset_dcd_count_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(top = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewResetDcdCountDialog( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + ResetDcdCountDialog( + //mainViewModel = mainViewModel + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/VerticallyRotatedCharacters.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/VerticallyRotatedCharacters.kt new file mode 100644 index 0000000..f2f15d8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/dcd/VerticallyRotatedCharacters.kt @@ -0,0 +1,71 @@ +package com.laseroptek.raman.ui.screens.home.dcd + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.utils.DefaultDispatcherProvider + + +@Composable +fun VerticallyRotatedCharacters( + text: String = "", + modifier: Modifier = Modifier, + charStyle: TextStyle = TextStyle.Default.copy(fontSize = 8.sp) +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, // How the column aligns its children (each Text) + verticalArrangement = Arrangement.spacedBy(-4.dp) // Optional: space between rotated characters + ) { + text.forEach { char -> + Text( + text = char.toString(), + style = charStyle, + modifier = Modifier + .rotate(90f) // Rotate each character 90 degrees clockwise + .padding(horizontal = 0.dp) // Add some padding so chars don't touch if wide + ) + } + } +} + + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun VerticallyRotatedCharactersPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + VerticallyRotatedCharacters( + //mainViewModel = mainViewModel + text = "COOLER TITLE" + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/energy/EnergyDetectPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/energy/EnergyDetectPopup.kt new file mode 100644 index 0000000..700a7b3 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/energy/EnergyDetectPopup.kt @@ -0,0 +1,693 @@ +package com.laseroptek.raman.ui.screens.home.energy + +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.const.CMD +import com.laseroptek.raman.const.READ_WRITE +import com.laseroptek.raman.const.SHOW_ENERGY_DETECT_CLOSE +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.serial.EnergyControl +import com.laseroptek.raman.data.model.serial.EnergyControlStop +import com.laseroptek.raman.data.model.serial.EnergyHandpiece +import com.laseroptek.raman.data.model.serial.EnergyMeasured +import com.laseroptek.raman.data.model.serial.HandPiece +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +/* +enum class EnergyDetectStatus { + READY, // INITIAL (gray - before check) + START, // START (yellow - ready to start or stopped) + STOP, // STOP (red - check started) + ERROR, // ERROR occured + EXIT, // CHECK FINISHED EXIT +} +*/ + +@Composable +fun EnergyDetectPopup( + title: String = stringResource(R.string.enery_check_title), + mainViewModel: MainViewModel = hiltViewModel(), + onClick: () -> Unit = {} +) { + val context = LocalContext.current + + val handPiece by mainViewModel.handPiece.collectAsState() + val energyHandpiece by mainViewModel.energyHandpiece.collectAsState() + val energyControl by mainViewModel.energyControl.collectAsState() + + val energyMeasured by mainViewModel.energyMeasured.collectAsState() + val energyMeasuredWrite by mainViewModel.energyMeasuredWrite.collectAsState() + val energyDetectRefer2 by mainViewModel.energyDetectRefer2.collectAsState() + + val voltage: Int = mainViewModel.getVoltage(10f, 10f) + + // Data class to hold all UI state variables for clarity + data class PopupState( + val buttonBackground: Brush, + val buttonBorderColor: Color, + val buttonDropShadowColor: Color, + val buttonInnerShadowColor: Color, + val checkHandpieceBackgroundColor: Color, + val checkHandpieceBorderColor: Color, + val checkDetectorBackgroundColor: Color, + val checkDetectorBorderColor: Color, + val errorMessage: String, + val buttonTitle: String, + val buttonTextColor: Color, + val resultMessage: String, + val resultMessageColor: Color, + val resultBackgroundColor: Color, + val resultBorderColor: Color, + ) + + // Default state for initialization and unknown cases + val initialState = PopupState( + buttonBackground = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(42, 37, 20), + 1.0f to Color(72, 63, 34), + ) + ), + buttonBorderColor = Color(102, 87, 8), + buttonDropShadowColor = Color(0, 0, 0).copy(alpha = 0.16f), + buttonInnerShadowColor = Color(123, 117, 95), + checkHandpieceBackgroundColor = Color(239, 68, 1), + checkHandpieceBorderColor = Color(185, 28, 28), + checkDetectorBackgroundColor = Color.Transparent, + checkDetectorBorderColor = Color(163,163,163), + errorMessage = "Please the tip connect to the handpiece.", + buttonTitle = "Start", + buttonTextColor = Color.White, + resultMessage = "", + resultMessageColor = Color(34,197,94), + resultBackgroundColor = Color(248,255,255), + resultBorderColor = Color(34,197,94) + ) + + val failState = PopupState( + buttonBackground = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(250, 250, 250), + 1.0f to Color(209, 209, 209), + ) + ), + buttonBorderColor = Color(199, 199, 199), + buttonDropShadowColor = Color(0, 0, 0).copy(alpha = 0.16f), + buttonInnerShadowColor = Color(255,255,255), + checkHandpieceBackgroundColor = Color(74, 222, 128), + checkHandpieceBorderColor = Color(22, 163, 74), + checkDetectorBackgroundColor = Color(74, 222, 128), + checkDetectorBorderColor = Color(22, 163, 74), + errorMessage = "", + buttonTitle = "Exit", + resultMessage = "Fail", + buttonTextColor = Color.Black, + resultMessageColor = Color(220,38,38), + resultBackgroundColor = Color(255,250,250), + resultBorderColor = Color(248,113,113) + ) + + val goodState = PopupState( + buttonBackground = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(250, 250, 250), + 1.0f to Color(209, 209, 209), + ) + ), + buttonBorderColor = Color(199, 199, 199), + buttonDropShadowColor = Color(0, 0, 0).copy(alpha = 0.16f), + buttonInnerShadowColor = Color(255,255,255), + checkHandpieceBackgroundColor = Color(74, 222, 128), + checkHandpieceBorderColor = Color(22, 163, 74), + checkDetectorBackgroundColor = Color(74, 222, 128), + checkDetectorBorderColor = Color(22, 163, 74), + errorMessage = "", + buttonTitle = "Exit", + resultMessage = "Good", + buttonTextColor = Color.Black, + resultMessageColor = Color(22,163,74), + resultBackgroundColor = Color(248,255,250), + resultBorderColor = Color(34,197,94), + ) + + Timber.d( + "handPiece: %d, energyHandpiece: 0x%X, energyControl: 0x%X", + handPiece.type, energyHandpiece.status, energyControl.status + ) + + val currentState = when { + //////////////////////////////////////////////////// + // 1 INITIAL - NO HANDPIECE + //////////////////////////////////////////////////// + handPiece.type == 0 -> initialState // Uses the default defined above + + //////////////////////////////////////////////////// + // 1.2 INITIAL - H/P CONNECTED, DETECTOR CHECKING + //////////////////////////////////////////////////// + energyHandpiece.status != 0x47 -> initialState.copy( + checkHandpieceBackgroundColor = Color(74, 222, 128), + checkHandpieceBorderColor = Color(22, 163, 74), + checkDetectorBackgroundColor = Color(239, 68, 1), + checkDetectorBorderColor = Color(185, 28, 28), + errorMessage = "Please the handpiece inserted into the detector." + ) + + //////////////////////////////////////////////////// + // DETECTOR IS GOOD (status = 0x47), CHECKING energyControl + // Ready State + energyControl.status == 0x00 || energyControl.status == 0x42 -> initialState.copy( + checkHandpieceBackgroundColor = Color(74, 222, 128), + checkHandpieceBorderColor = Color(22, 163, 74), + checkDetectorBackgroundColor = Color(74, 222, 128), + checkDetectorBorderColor = Color(22, 163, 74), + errorMessage = "", + buttonTitle = "Start", + resultMessage = "", + buttonTextColor = Color.White, + ) + + // Start -> [Stop] State + energyControl.status == 0x41 -> initialState.copy( + buttonBackground = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(207, 38, 38), + 1.0f to Color(151, 14, 14), + ) + ), + buttonBorderColor = Color(233, 58, 58), + buttonDropShadowColor = Color(0, 0, 0).copy(alpha = 0.16f), + buttonInnerShadowColor = Color(255, 173, 173), + checkHandpieceBackgroundColor = Color(74, 222, 128), + checkHandpieceBorderColor = Color(22, 163, 74), + checkDetectorBackgroundColor = Color(74, 222, 128), + checkDetectorBorderColor = Color(22, 163, 74), + errorMessage = "", + buttonTitle = "Stop", + resultMessage = "", + buttonTextColor = Color.White, + ) + + // Finish - Fail State + energyControl.status != 0x47 -> failState + + //////////////////////////////////////////////////////////////////////////////////////////// + // Energy Control Good + + // Finish - Good State + energyMeasured.measured == 0 -> { + Timber.d("Waiting for result") + goodState.copy( + resultMessage = "Measuring", + ) + } + + // Condition 2: Good + energyMeasured.measured < (energyMeasuredWrite.measured * energyDetectRefer2.good) -> { + Timber.d("Good for result") + goodState.copy( + resultMessage = "Good", + ) + } + + // Condition 3: Acceptable + energyMeasured.measured < (energyMeasuredWrite.measured * energyDetectRefer2.acceptable) -> { + Timber.d("Acceptable for result") + goodState.copy( + resultMessage = "Acceptable", + ) + } + + // Condition 4: Bad + energyMeasured.measured < (energyMeasuredWrite.measured * energyDetectRefer2.bad) -> { + Timber.d("Bad for result") + failState.copy( + resultMessage = "Bad", + ) + } + + // Final 'else' branch for the 'when' statement + else -> { + // Unknown State -> Defaults to Bad + Timber.d("Bad (Unknown) for result") + failState.copy( + resultMessage = "Bad", + ) + } + } + + FullScreen( + contentAlignment = Alignment.Center, + backgroundColor = Color.Black.copy(alpha = 0.66f) + ) { + Column( + modifier = Modifier + .width(400.px.dp) + .wrapContentHeight() + .clip(RoundedCornerShape(28.px.dp)) + .background(Color(255, 255, 255)) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0, 0, 0).copy(alpha = 1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + //Spacer(modifier = Modifier.size(24.px.dp)) + // Close + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.px.dp) + .padding(top = 10.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.Bottom, + ) { + // Close (X) + if (SHOW_ENERGY_DETECT_CLOSE) { + IconButton( + onClick = { + Timber.d("Now exit Energy Detect") + onClick.invoke() + }, + modifier = Modifier.size(20.px.dp, 20.px.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, + modifier = Modifier.size(14.px.dp), + contentScale = ContentScale.Crop + ) + } + } + } + + // title + Row( + modifier = Modifier + .size(352.px.dp, 32.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Thin, + fontSize = 24.px.sp, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + + // error message + if (currentState.errorMessage != "") { + Spacer(modifier = Modifier.size(20.px.dp)) + + Row( + modifier = Modifier + .size(352.px.dp, 44.px.dp) + .clip(RoundedCornerShape(14.px.dp)) + .background(Color(220, 38, 38).copy(alpha = 0.15f)), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.size(6.px.dp)) + + // Icon + Image( + painter = painterResource(id = R.drawable.ic_error), + contentDescription = "Background Image", + modifier = Modifier.size(20.px.dp), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.size(10.px.dp)) + + // errorMessage + Text( + text = currentState.errorMessage, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 12.px.sp, + color = Color(239,68,68), + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.weight(1f)) + } + } + + Spacer(modifier = Modifier.size(24.px.dp)) + + // Check the handpiece. + Row( + modifier = Modifier + .size(352.px.dp, 41.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.size(10.px.dp)) + + Box( + modifier = Modifier + .size(16.px.dp) // Set the size to 16x16 + .clip(CircleShape) // Clip the Box to a circle + .background(currentState.checkHandpieceBackgroundColor) + .border( + 1.px.dp, + color = currentState.checkHandpieceBorderColor, + shape = CircleShape + ) + ) + + Spacer(modifier = Modifier.size(15.px.dp)) + + Text( + text = "Check the handpiece.", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + // Check the energyHandpiece. + Row( + modifier = Modifier + .size(352.px.dp, 41.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.size(10.px.dp)) + + Box( + modifier = Modifier + .size(16.px.dp) // Set the size to 16x16 + .clip(CircleShape) // Clip the Box to a circle + .background(currentState.checkDetectorBackgroundColor) + .border( + 1.px.dp, + color = currentState.checkDetectorBorderColor, + shape = CircleShape + ) + ) + + Spacer(modifier = Modifier.size(15.px.dp)) + + Text( + text = "Check the energyHandpiece.", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + if (currentState.resultMessage != "") { + Spacer(modifier = Modifier.size(24.px.dp)) + + Column( + modifier = Modifier + .size(352.px.dp, 92.px.dp) + .clip(RoundedCornerShape(12.px.dp)) + .background(currentState.resultBackgroundColor) + .border( + 1.px.dp, + color = currentState.resultBorderColor, + shape = RoundedCornerShape(12.px.dp) + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Energy check result + Text( + text = "Energy check result", + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 14.px.sp, + color = Color(113,113,122), + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.size(4.px.dp)) + + // Result + Text( + text = currentState.resultMessage, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 22.px.sp, + color = currentState.resultMessageColor, + textAlign = TextAlign.Center, + ) + } + } + + Spacer(modifier = Modifier.size(24.px.dp)) + + // button + Row( + modifier = Modifier + .size(352.px.dp, 40.px.dp) + .clip(RoundedCornerShape(40.px.dp)) + .background( + brush = currentState.buttonBackground + ) + .border( + 1.px.dp, + color = currentState.buttonBorderColor, + shape = RoundedCornerShape(40.px.dp) + ) + .dropShadow( + shape = RoundedCornerShape(40.px.dp), + color = currentState.buttonDropShadowColor, + blur = 4.px.dp, + offsetX = 0.px.dp, + offsetY = 4.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(40.px.dp), + color = currentState.buttonInnerShadowColor, + blur = 1.px.dp, + offsetY = 1.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + modifier = Modifier.fillMaxSize(), + onClick = { + Timber.d("Button Clicked") + + Timber.d("handPiece: %d, energyHandpiece: 0x%X, energyControl: 0x%X, energyMeasured: %d", + handPiece.type, energyHandpiece.status, energyControl.status, energyMeasured.measured + ) + + if (handPiece.type != 0 && energyHandpiece.status == 0x47) { + // send start (energy control) + if (energyControl.status == 0x00 || energyControl.status == 0x42) { // init or stop + // init + Timber.d("Now start Energy Detect (voltage = ${voltage})") + Toast.makeText( + context, + "EnergyControl Start", + Toast.LENGTH_LONG + ).show() + + mainViewModel.setEnergyControl( + EnergyControl( + type = 0x03, + status = 0x41, // 'A': Start + ) + ) + + // initialize + mainViewModel.setEnergyMeasured( + EnergyMeasured( + type = 0x04, + measured = 0, + measured2 = 0, + ) + ) + + // ready: send start + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.ENERGY_DETECT, + EnergyControl( + type = 0x03, + status = 0x41, // 'A': Start + voltage = voltage, // 10ms, 10J + ) + ) + } + else if (energyControl.status == 0x41) { + Timber.d("Now Stop Energy Detect") + Toast.makeText( + context, + "EneryControl Stop", + Toast.LENGTH_LONG + ).show() + + // init + mainViewModel.setEnergyControl( + EnergyControl( + type = 0x03, + status = 0x42, // 'B': Stop + ) + ) + + mainViewModel.setEnergyMeasured( + EnergyMeasured( + type = 0x04, + measured = 0 + ) + ) + + // start: send stop + mainViewModel.txPacket( + READ_WRITE.WRITE, + CMD.ENERGY_DETECT, + EnergyControlStop( + type = 0x03, + status = 0x42, // 'B': Stop + ) + ) + } else if (energyControl.status == 0x47 || energyControl.status == 0x4E) { + // 0x47 ('G') or 0x4E ('N') - Not good -> Finsih + Timber.d("Now exit Energy Detect") + onClick.invoke() + } else { + Timber.e("Unknown energy control status: ${energyControl.status}") + + Toast.makeText( + context, + "Unknown energy control status: ${energyControl.status}.", + Toast.LENGTH_LONG + ).show() + } + } + else { + Timber.e("Unable to start control - h/p: ${handPiece.type}, energyHandpiece :${energyHandpiece.status}") + + val msg = + if (handPiece.type == 0) + "Please the tip connect to the handpiece." + else + "Please the handpiece inserted into the detector." + + Toast.makeText( + context, + msg, + Toast.LENGTH_LONG + ).show() + } + }, + ) { + Text( + text = currentState.buttonTitle, + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = currentState.buttonTextColor + ) + } + } + + Spacer(modifier = Modifier.size(24.px.dp)) + } + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1280dp,height=720dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewEnergyDetectPopup( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + // test + mainViewModel.setHandPiece(HandPiece(type = 0)) + mainViewModel.setEnergyHandpiece(EnergyHandpiece(status = 0x47)) // 0x47('G'), 0x4E('N') + mainViewModel.setEnergyControl(EnergyControl(status = 0x47)) // 0x47('G'), 0x4E('N') + + EnergyDetectPopup( + title = stringResource(R.string.enery_check_title), + mainViewModel = mainViewModel, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/DeleteConfirmDialog.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/DeleteConfirmDialog.kt new file mode 100644 index 0000000..cc6b8d4 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/DeleteConfirmDialog.kt @@ -0,0 +1,143 @@ +package com.laseroptek.raman.ui.screens.home.preset + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun DeleteConfirmDialog( + title: String = stringResource(R.string.delete_confirm_title), + onClick: (Boolean) -> Unit= {} +) { + FullScreen { + Column( + modifier = Modifier + .size(312.px.dp, 132.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Title + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.px.dp) + .padding(top = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = RobotoTypography.headlineLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 20.px.sp + ) + } + + // Two Button + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("Ok Clicked") + onClick(true) + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewDeleteConfirmDialog() { + DeleteConfirmDialog( + onClick = {} + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetButton.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetButton.kt new file mode 100644 index 0000000..d4491f4 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetButton.kt @@ -0,0 +1,146 @@ +package com.laseroptek.raman.ui.screens.home.preset + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun PresetButton( + modifier: Modifier = Modifier, // Default size set here + fontSize: Int = 18, + no: Int = 0, + selected: Int = 0, + onClick: (Int) -> Unit = {}, + onLongClick: (Int) -> Unit = {}, +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(50.px.dp)) // Note: This fixed radius might need adjustment if size changes significantly + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, // This removes the ripple effect, same as noRippleClickable + onClick = { + Timber.d("click on PresetButton: $no") + onClick.invoke(no) + }, + onLongClick = { + Timber.d("Long click on PresetButton: $no") + onLongClick.invoke(no) + } + ) + .border( + border = BorderStroke( + width = 2.dp, + brush = SolidColor( + Color(131, 131, 131) + ) + ), + shape = RoundedCornerShape(50.dp) // Note: This shape's radius might also need adjustment. + ) + .dropShadow( + shape = RoundedCornerShape(50.px.dp), // Note: Fixed radius + color = if (selected == no) { + Color(213, 167, 15) + } else { + Color(0, 0, 0).copy(alpha = 0.27f) + }, + blur = 5.px.dp, + offsetY = 2.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(50.px.dp), // Note: Fixed radius + color = if (selected == no) { + Color(213, 167, 15) + } else { + Color(0, 0, 0).copy(alpha = 0.15f) + }, + blur = 5.px.dp, + offsetY = -2.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(50.px.dp), // Note: Fixed radius + color = if (selected == no) { + Color(213, 167, 15) + } else { + Color(0, 0, 0).copy(alpha = 0.5f) + }, + blur = 1.px.dp, + offsetY = -1.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .background( + if (selected == no) { + Brush.verticalGradient( + colors = listOf( + Color(255, 239, 207), + Color(255, 211, 124), + ) + ) + } else { + Brush.verticalGradient( + colors = listOf( + Color(192, 192, 192), + Color(246, 246, 246), + ) + ) + }, + shape = RoundedCornerShape(50.px.dp) // Note: Fixed radius + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = no.toString(), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = fontSize.px.sp, // Note: Fixed font size might need adjustment if button size changes significantly + color = Color(49,46,38), + textAlign = TextAlign.Center, + ) + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=portrait" +) +@Composable +fun PreviewPresetButton() { + PresetButton( + modifier = Modifier.size(50.dp) + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetIconButton.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetIconButton.kt new file mode 100644 index 0000000..3aa79ad --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetIconButton.kt @@ -0,0 +1,138 @@ +package com.laseroptek.raman.ui.screens.home.preset + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.PresetButtonType +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + +@Composable +fun PresetIconButton( + modifier: Modifier = Modifier, // Default size set here + type: PresetButtonType = PresetButtonType.SAVE, + selected: Boolean = false, + onClick: () -> Unit = {}, +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(50.px.dp)) // Note: This fixed radius might need adjustment if size changes significantly + .noRippleClickable { + Timber.d("onClick") + onClick.invoke() + } + .border( + border = BorderStroke( + width = 2.dp, + brush = SolidColor( + Color(131, 131, 131) + ) + ), + shape = RoundedCornerShape(50.dp) // Note: This shape's radius might also need adjustment. + // Consider using 50.px.dp like other shapes for consistency. + ) + .dropShadow( + shape = RoundedCornerShape(50.px.dp), // Note: Fixed radius + color = if (selected) { + Color(213,167,15) + } else { + Color(0, 0, 0).copy(alpha = 0.27f) + }, + blur = 5.px.dp, + offsetY = 2.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(50.px.dp), // Note: Fixed radius + color = if (selected) { + Color(213,167,15) + } else { + Color(0, 0, 0).copy(alpha = 0.15f) + }, + blur = 5.px.dp, + offsetY = -2.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(50.px.dp), // Note: Fixed radius + color = Color(0, 0, 0).copy(alpha = 0.5f), + blur = 1.px.dp, + offsetY = -1.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .background( + if (selected) { + Brush.verticalGradient( + colors = listOf( + Color(255,239,207), + Color(255,211,124), + ) + ) + } else { + Brush.verticalGradient( + colors = listOf( + Color(192,192,192), + Color(246,246,246), + ) + ) + }, + shape = RoundedCornerShape(50.px.dp) // Note: Fixed radius + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = + if (type == PresetButtonType.SAVE) { + R.drawable.ic_preset_save + } else { + R.drawable.ic_preset_load + } + ), + contentDescription = "", + modifier = Modifier + .size(20.px.dp), + contentScale = ContentScale.Crop + ) + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=portrait" +) +@Composable +fun PreviewPresetIconButton() { + PresetIconButton( + modifier = Modifier.size(50.dp), + type = PresetButtonType.SAVE, + onClick = {} + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetLoadPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetLoadPopup.kt new file mode 100644 index 0000000..cd617ab --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetLoadPopup.kt @@ -0,0 +1,1359 @@ +package com.laseroptek.raman.ui.screens.home.preset + +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.const.UpDownState +import com.laseroptek.raman.const.handPieceTypes +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.Preset +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.button.GradientImageButton +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.ui.theme.robotoTextStyle +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.ext.simpleVerticalScrollbar +import kotlinx.coroutines.launch +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PresetLoadPopup( + mainViewModel: MainViewModel = hiltViewModel(), + presetViewModel: PresetViewModel = hiltViewModel(), + title: String = stringResource(R.string.load_title), + onClick: (Boolean) -> Unit= {}, +) { + val scope = rememberCoroutineScope() + + var showDeleteConfirmDialog by remember { mutableStateOf(false) } + + val context = LocalContext.current + val focusManager = LocalFocusManager.current + + val selectedPresetIndex by presetViewModel.selectedPresetIndex.collectAsState() + val isEditMode by presetViewModel.isEditMode.collectAsState() + val contentAlignment = if (isEditMode) { + Arrangement.Top + } else { + Arrangement.Center + } + + val loadPreset = { + // Get the currently connected handpiece type from the MainViewModel + val currentHandPieceType = mainViewModel.handPiece.value.type + + // Filter the main preset list based on the required conditions + val filteredList = mainViewModel.presetList.value.filter { preset -> + // This includes presets for the current handpiece OR common presets (type 0) + preset.handPieceType == currentHandPieceType || preset.handPieceType == 0 + } + + // init + presetViewModel.setIsEditMode(false) + + // Now set the presetViewModel's list with this newly filtered list + presetViewModel.setPresetListSorted(filteredList) + Timber.d("loadPreset function executed. Filtered list count: ${filteredList.size}") + } + + + LaunchedEffect(Unit) { + //presetViewModel.clearPreset() + loadPreset() + + focusManager.clearFocus(force = true) // Hide the keyboard + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + FullScreen { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = contentAlignment + ) { + Spacer(modifier = Modifier.height(50.px.dp)) + + // 전체 팝업 윈도 + Column( + modifier = Modifier + .size(760.px.dp, 360.px.dp) + .clip(RoundedCornerShape(16.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(10.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Title - (X 버튼 ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ){ + IconButton( + onClick = { presetViewModel.clearPreset(); onClick.invoke(false) }, + modifier = Modifier.size(24.px.dp, 24.px.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, + modifier = Modifier.size(24.px.dp), + contentScale = ContentScale.Crop + ) + } + } + + // Table 상단 + Row( + modifier = Modifier + .fillMaxWidth() + .height(32.px.dp) + .clip(RoundedCornerShape(topStart = 4.px.dp, topEnd = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ) + .border( + width = 1.4.px.dp, + //color = Color.White, + brush = Brush.linearGradient( + colors = listOf( + Color(183, 142, 9), + Color(75, 63, 23) + ), + //start = Offset(0f, 0f), // Optional: Start at the top-left + //end = Offset(200f, 200f) // Optional: End at the bottom-right + ), + shape = RoundedCornerShape(topStart = 4.px.dp, topEnd = 4.px.dp), + ) + .dropShadow( + shape = RoundedCornerShape(topStart = 4.px.dp, topEnd = 4.px.dp), + color = Color.Black.copy(alpha = 0.12f), + blur = 10.px.dp, + offsetX = 0.px.dp, + offsetY = 2.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Name + Column( + modifier = Modifier + .fillMaxSize() + .weight(2f) + .clip(RoundedCornerShape(topStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topStart = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Name", + style = RobotoTypography.bodyMedium, + fontSize = 10.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // HandPiece + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topStart = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "HandPiece (mm)", + style = RobotoTypography.bodyMedium, + fontSize = 10.px.sp, + lineHeight = 10.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // PulseWidth + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topStart = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "PulseWidth (ms)", + style = RobotoTypography.bodyMedium, + fontSize = 10.px.sp, + lineHeight = 10.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // Fluence + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topStart = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Fluence (J/cm²)", + style = RobotoTypography.bodyMedium, + fontSize = 10.px.sp, + lineHeight = 10.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // Repetition + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topStart = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Repetition (Hz)", + style = RobotoTypography.bodyMedium, + fontSize = 10.px.sp, + lineHeight = 10.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // Priority + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topStart = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Priority", + style = RobotoTypography.bodyMedium, + fontSize = 10.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + } + + // Table 하단 + Row( + modifier = Modifier + .fillMaxWidth() + .height(200.px.dp) // 230 + .clip(RoundedCornerShape(bottomStart = 4.px.dp, bottomEnd = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ) + .border( + width = 1.4.px.dp, + //color = Color.White, + brush = Brush.linearGradient( + colors = listOf( + Color(183, 142, 9), + Color(75, 63, 23) + ), + //start = Offset(0f, 0f), // Optional: Start at the top-left + //end = Offset(200f, 200f) // Optional: End at the bottom-right + ), + shape = RoundedCornerShape(bottomStart = 4.px.dp, bottomEnd = 4.px.dp), + ) + .dropShadow( + shape = RoundedCornerShape(bottomStart = 4.px.dp, bottomEnd = 4.px.dp), + color = Color.Black.copy(alpha = 0.12f), + blur = 10.px.dp, + offsetX = 0.px.dp, + offsetY = 2.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Preset Lists. + PresetLoadList( + modifier = Modifier.fillMaxSize(), + presetViewModel = presetViewModel, + ) + } + + Spacer(modifier = Modifier.height(10.px.dp)) + + // Buttons(Preset1..5, Delete, Edit, Save) + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp), // 88 + //.padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.weight(1f)) + + if (isEditMode) { + //////////////////////////////////////////////////// + // Edit Mode + + // Preset Delete (Delete confirm popup) + Box( + modifier = Modifier + .noRippleClickable(onClick = { + Timber.d("onClick - Preset Delete") + showDeleteConfirmDialog = true + }) + .size(40.px.dp) + .background(Color.Transparent) + ) { + Image( + painter = painterResource(id = R.drawable.ic_delete_btn), + contentDescription = "Preset Load", + modifier = Modifier + .size(40.px.dp) + .align(Alignment.BottomCenter), + contentScale = ContentScale.Crop + ) + } + + Spacer(Modifier.width(10.px.dp)) + + // Preset Cancel (Reload selected item from mainViewModel) + Box( + modifier = Modifier + .noRippleClickable(onClick = { + Timber.d("onClick - Preset Cancel") + + // Find preset Item with selectedIndex from PresetViewModel + val selectedPreset = + presetViewModel.getPreset(selectedPresetIndex) + if (selectedPreset == null) { + Timber.e("refreshPresetById: Could not find preset with selectedPresetIndex $selectedPresetIndex in PresetViewModel.") + Toast.makeText( + context, + "Could not find preset with selectedPresetIndex $selectedPresetIndex in PresetViewModel.", + Toast.LENGTH_LONG + ).show() + return@noRippleClickable + } + + // Find the preset from MainViewModel's list. + val masterPreset = + mainViewModel.presetList.value.find { it.id == selectedPreset.id } + if (masterPreset == null) { + Timber.w("refreshPresetById: Could not find preset with ID ${selectedPreset.id} in MainViewModel.") + Toast.makeText( + context, + "refreshPresetById: Could not find preset with ID ${selectedPreset.id} in MainViewModel.", + Toast.LENGTH_LONG + ).show() + return@noRippleClickable + } + + // Set the masterPreset to PresetViewModel + presetViewModel.setPreset(masterPreset) + + /* + Toast.makeText( + context, + "Preset list update completed.", + Toast.LENGTH_LONG + ).show() + */ + }) + .size(40.px.dp) + .background(Color.Transparent) + ) { + Image( + painter = painterResource(id = R.drawable.ic_cancel_btn), + contentDescription = "Preset Cancel", + modifier = Modifier + .size(40.px.dp) + .align(Alignment.BottomCenter), + contentScale = ContentScale.Crop + ) + } + + Spacer(Modifier.width(10.px.dp)) + + // Preset Save Confirm + Box( + modifier = Modifier + .noRippleClickable(onClick = { + // save presetList to mainViewModel + Timber.d("onClick - Confirm Save") + + // Check empty names and conflict priority exist in the preset viewmodel's presetList + val listToValidate = presetViewModel.presetList.value + + // Check for any presets with an empty name + val hasEmptyName = + listToValidate.any { it.name.trim().isEmpty() } + if (hasEmptyName) { + Toast.makeText( + context, + "One or more presets have an empty name. Please correct it.", + Toast.LENGTH_LONG + ).show() + return@noRippleClickable // Stop the process + } + + // Check 2: Are there any duplicate names among the non-empty ones? + val nameConflicts = listToValidate + .filter { + it.name.trim().isNotEmpty() + } // Only consider non-empty names + .groupBy { it.name.trim() } // Group by name + .any { it.value.size > 1 } // See if any group has duplicates + + if (nameConflicts) { + Toast.makeText( + context, + "There are duplicate preset names. Please ensure each name is unique.", + Toast.LENGTH_LONG + ).show() + return@noRippleClickable // Stop the process + } + + // Check for duplicate priorities (ignoring priority 0) + val priorityConflicts = listToValidate + .filter { it.priority > 0 } // Only consider prioritized items + .groupBy { it.priority } // Group them by priority + .any { it.value.size > 1 } // Check if any group is larger than 1 + + if (priorityConflicts) { + Toast.makeText( + context, + "There are duplicate priorities. Please ensure each priority is unique.", + Toast.LENGTH_LONG + ).show() + return@noRippleClickable // Stop the process + } + + Timber.d("Validation successful. Saving list to MainViewModel.") + + // Save presetList to mainViewModel + mainViewModel.setPresetList(presetViewModel.presetList.value) + + scope.launch { + mainViewModel.savePresetListToPreference() + } + + /* + Toast.makeText( + context, + "Preset edit done.", + Toast.LENGTH_LONG + ).show() + */ + + // remove preset + presetViewModel.clearPreset() + + // Toggle edit mode + //presetViewModel.setIsEditMode(false) + + loadPreset() + }) + .size(40.px.dp) + .background(Color.Transparent) + ) { + Image( + painter = painterResource(id = R.drawable.ic_save_btn), + contentDescription = "Preset Load", + modifier = Modifier + .size(40.px.dp) + .align(Alignment.BottomCenter), + contentScale = ContentScale.Crop + ) + } + } else { + //////////////////////////////////////////////////// + // Select Mode - hide Keyboard + focusManager.clearFocus(force = true) // Hide the keyboard + + // [Edit Btn] + Box( + modifier = Modifier + .noRippleClickable(onClick = { + Timber.d("onClick - Preset Edit") + presetViewModel.setIsEditMode(true) // //presetViewModel.toggleIsEditMode() + }) + .size(40.px.dp) + .background(Color.Transparent) + ) { + Image( + painter = painterResource(id = R.drawable.ic_edit_btn), + contentDescription = "Preset Load", + modifier = Modifier + .size(40.px.dp) + .align(Alignment.BottomCenter), + contentScale = ContentScale.Crop + ) + } + + Spacer(Modifier.width(10.px.dp)) + + // [Load Btn] + Box( + modifier = Modifier + .noRippleClickable(onClick = { + Timber.d("onClick - Preset Load") + + val selectedPreset = + presetViewModel.getPreset(selectedPresetIndex) + val priority = + selectedPreset?.priority ?: 0 + + Timber.d("onClick - Preset Load ($priority)") + +// if (priority > 0) { // TODO : 검증 필요 + mainViewModel.setSelectedPresetIndex( priority ) + mainViewModel.applyPreset(priority) + presetViewModel.clearPreset() + onClick.invoke(false) +// } else { +// Timber.d("SKIP - Preset Load ($priority)") +// } + }) + .size(40.px.dp) + .background(Color.Transparent) + ) { + Image( + painter = painterResource(id = R.drawable.ic_load_btn), + contentDescription = "Preset Load", + modifier = Modifier + .size(40.px.dp) + .align(Alignment.BottomCenter), + contentScale = ContentScale.Crop + ) + } + } + } + + Spacer(modifier = Modifier.height(10.px.dp)) + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Popup + if (showDeleteConfirmDialog) { + DeleteConfirmDialog( + title = stringResource(R.string.delete_confirm_title), + onClick = { ok -> + if (ok) { + Timber.d("onClick - Delete Confirm OK") + + // remove from View + presetViewModel.deletePreset(selectedPresetIndex) + + // reset index + if (selectedPresetIndex > 0) { + presetViewModel.setSelectedPresetIndex(selectedPresetIndex -1) + } else { + presetViewModel.setSelectedPresetIndex(0) + } + } else { + Timber.d("onClick - Delete Confirm Cancel") + } + showDeleteConfirmDialog = false + } + ) + + } + } +} + +@Composable +fun PresetLoadList( + modifier: Modifier = Modifier, + presetViewModel: PresetViewModel = hiltViewModel(), +) { + val selectedPresetIndex by presetViewModel.selectedPresetIndex.collectAsState() + val isEditMode by presetViewModel.isEditMode.collectAsState() + val listData by presetViewModel.presetList.collectAsState() + val state = rememberLazyListState(selectedPresetIndex) + + LazyColumn( + modifier = modifier.simpleVerticalScrollbar(state), + state = state + ) { + itemsIndexed( + items = listData, + key = { index, item -> item.id } + ) { index, presetItem -> + //val index = presets.size -1 - rIndex + Timber.d("index --> $index, priority: ${presetItem.priority}") + PresetLoadRow( + preset = presetItem, + index = index, + selectedPresetIndex = selectedPresetIndex, + isEditMode = isEditMode, + onItemClickListener = { idx -> + Timber.d("Preset Item Clicked(${idx}, index: ${index})") + if (!isEditMode) { + presetViewModel.setSelectedPresetIndex(idx) + } + }, + onNameChange = { itemIndex, newName -> + Timber.d("Preset onNameChange (${itemIndex}, index: ${index}, newName: ${newName})") + presetViewModel.updatePresetName(itemIndex, newName) + }, + onHandPieceTypeChange = { itemIndex, handPieceType, /* pulseWidth, fluence, repetition,*/ isUp -> + Timber.d("Preset onHandPieceTypeChange (${itemIndex}, index: ${index}, isUp: ${isUp})") + presetViewModel.updateHandPieceType(itemIndex, handPieceType, /* pulseWidth, fluence, repetition,*/ isUp) + }, + onPulseWidthChange = { itemIndex, handPieceType, pulseWidth, /* fluence, repetition,*/ isUp -> + Timber.d("Preset onPulseWidthChange (${itemIndex}, index: ${index}, pulseWidth: ${pulseWidth} isUp: ${isUp})") + presetViewModel.updatePulseWidth(itemIndex, handPieceType, pulseWidth, /* fluence, repetition,*/ isUp) + }, + onFluenceChange = { itemIndex, handPieceType, pulseWidth, /* fluence, */ repetition, isUp -> + Timber.d("Preset onFluenceChange (${itemIndex}, index: ${index}, pulseWidth: ${pulseWidth}, repetition: ${repetition}, isUp: ${isUp})") + presetViewModel.updateFluence(itemIndex, handPieceType, pulseWidth, /* fluence, */ repetition, isUp) + }, + onRepetitionChange = { itemIndex, handPieceType, pulseWidth, fluence, repetition, isUp -> + Timber.d("Preset onRepetitionChange (${itemIndex}, index: ${index}, pulseWidth: ${pulseWidth}, fluence: ${fluence}, repetition: ${repetition}, isUp: ${isUp})") + presetViewModel.updateRepetition(itemIndex, handPieceType, pulseWidth, fluence, repetition, isUp) + }, + onPriorityChange = { itemIndex, priority, isUp -> + Timber.d("Preset onPriorityChange (${itemIndex}, index: ${index}, priority: ${priority}, isUp: ${isUp})") + presetViewModel.updatePriority(itemIndex, priority, isUp) + }, + ) + + // divider + HorizontalDivider( + color = Color(228, 228, 231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxWidth() + .height(1.px.dp) + ) + } + } +} + +@Composable +fun PresetLoadRow( + preset: Preset = Preset(), + index: Int = 0, + selectedPresetIndex: Int = 0, + isEditMode: Boolean = false, + onItemClickListener: (Int) -> Unit, + onNameChange: (Int, String) -> Unit, + onHandPieceTypeChange: (Int, Int, UpDownState) -> Unit, + onPulseWidthChange: (Int, Int, Float, UpDownState) -> Unit, + onFluenceChange: (Int, Int, Float, Float, UpDownState) -> Unit, + onRepetitionChange: (Int, Int, Float, Float, Float, UpDownState) -> Unit, + onPriorityChange: (Int, Int, UpDownState) -> Unit, +) { + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + var textFieldValue by remember(preset.name) { + mutableStateOf( + TextFieldValue( + text = preset.name, + selection = TextRange(preset.name.length) // Default selection to the end + ) + ) + } + + Row( + modifier = Modifier + .noRippleClickable { onItemClickListener(index) } + /* + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, // This removes the ripple effect + onClick = { + Timber.d("Short click on row $index") + }, + onLongClick = { + onItemClickListener(index) + Timber.d("Long click on row $index") + } + ) + */ + .fillMaxWidth() + .height(37.px.dp) + //.border(width = 2.px.dp, color = Color(200,56,4)), + .background( + color = if (selectedPresetIndex == index) Color(254, 245, 216) else Color.White + ), + verticalAlignment = Alignment.CenterVertically + ) { + // Name - Value + Column( + modifier = Modifier + .fillMaxSize() + .weight(2f) + .background(Color.Transparent), + //.border(width = 1.px.dp, color = Color(228,228,231)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (isEditMode && index == selectedPresetIndex) { + // EditBox + BasicTextField( + value = textFieldValue, + onValueChange = { newTextFieldValue -> + textFieldValue = newTextFieldValue + Timber.d("newText for index $index, ${newTextFieldValue.text}") + onNameChange(index, newTextFieldValue.text) }, + modifier = Modifier + .fillMaxWidth() // Fill the width of the parent Row + .weight(1f) // If other elements were in the Row, this helps fill space + .focusRequester(focusRequester), + textStyle = robotoTextStyle.copy( // Use your existing text style + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.Center // This centers the input text + ), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, // KeyboardType.Text, + imeAction = ImeAction.Done, + //autoCorrect = false + ), + keyboardActions = KeyboardActions(onDone = { + focusManager.clearFocus(force = true) + }), + decorationBox = { innerTextField -> + // This Box helps in positioning the placeholder and the input area + Box( + modifier = Modifier.fillMaxWidth(), // Ensure the Box takes full width for textAlign + contentAlignment = Alignment.Center // Center placeholder and input text + ) { + innerTextField() // This is where the actual editable text appears + } + } + ) + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + } else { + Text( + text = preset.name, // preset.name, + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // HandPiece - Value + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background(Color.Transparent), + //.border(width = 1.px.dp, color = Color(228,228,231)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row ( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = handPieceTypes.get(preset.handPieceType), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // PulseWidth - Value + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background(Color.Transparent), + //.border(width = 1.px.dp, color = Color(228,228,231)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row ( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_up, + onClick = { + // PulseWidth up + Timber.d("PulseWidth up") + onPulseWidthChange(index, preset.handPieceType, preset.pulseWidth, UpDownState.Up) + } + ) + Spacer(Modifier.weight(1f)) + } + + Text( + text = preset.pulseWidth.toString(), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_down, + onClick = { + // PulseWidth down + Timber.d("PulseWidth down") + onPulseWidthChange(index, preset.handPieceType, preset.pulseWidth, UpDownState.Down) + } + ) + Spacer(Modifier.weight(1f)) + } + } + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // Fluence - Value + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background(Color.Transparent), + //.border(width = 1.px.dp, color = Color(228,228,231)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row ( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_up, + onClick = { + // Fluence up + Timber.d("Fluence up") + onFluenceChange(index, preset.handPieceType, preset.pulseWidth, preset.fluence, UpDownState.Up) + } + ) + Spacer(Modifier.weight(1f)) + } + + Text( + text = preset.fluence.toString(), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_down, + onClick = { + // Fluence down + Timber.d("Fluence down") + onFluenceChange(index, preset.handPieceType, preset.pulseWidth, preset.fluence, UpDownState.Down) + } + ) + Spacer(Modifier.weight(1f)) + } + } + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // repetition - Value + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background(Color.Transparent), + //.border(width = 1.px.dp, color = Color(228,228,231)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row ( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_up, + onClick = { + // repetition up + Timber.d("repetition up") + onRepetitionChange(index, preset.handPieceType, preset.pulseWidth, preset.fluence, preset.repetition, UpDownState.Up) + } + ) + Spacer(Modifier.weight(1f)) + } + + Text( + text = preset.repetition.toString(), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_down, + onClick = { + // repetition down + Timber.d("repetition down") + onRepetitionChange(index, preset.handPieceType, preset.pulseWidth, preset.fluence, preset.repetition, UpDownState.Down) + } + ) + Spacer(Modifier.weight(1f)) + } + } + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // priority - Value + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background(Color.Transparent), + //.border(width = 1.px.dp, color = Color(228,228,231)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Row ( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_up, + onClick = { + // priority up + Timber.d("priority up") + onPriorityChange(index, preset.priority, UpDownState.Up) + } + ) + Spacer(Modifier.weight(1f)) + } + + + Text( + text = if (preset.priority == 0) "NONE" else preset.priority.toString(), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + + if (isEditMode && index == selectedPresetIndex) { + Spacer(Modifier.weight(1f)) + GradientImageButton( + id = R.drawable.ic_arrow_down, + onClick = { + // priority down + Timber.d("priority down") + onPriorityChange(index, preset.priority, UpDownState.Down) + } + ) + Spacer(Modifier.weight(1f)) + } + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Previews +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewPresetLoadPopup( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + Box { + PresetLoadPopup( + mainViewModel = mainViewModel, + ) + } +} + +/* +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewPresetLoadList( + presetViewModel:PresetViewModel = PresetViewModel() +) { + Box { + PresetLoadList( + presetViewModel = presetViewModel, + ) + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewPresetLoadRow( +) { + Box { + PresetLoadRow( + preset = Preset( + name = "Name" + ), + index = 0, + selectedPresetIndex = 0, + isEditMode = false, + onItemClickListener = { }, + onNameChange = { _, _ ->}, + onHandPieceTypeChange = { _, _, _ ->}, + onPulseWidthChange = { _, _, _, _ ->}, + onFluenceChange = { _, _, _, _, _ ->}, + onRepetitionChange = { _, _, _, _, _, _ ->}, + onPriorityChange = { _, _, _ ->}, + ) + } +} +*/ diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetSavePopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetSavePopup.kt new file mode 100644 index 0000000..542014b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetSavePopup.kt @@ -0,0 +1,783 @@ +package com.laseroptek.raman.ui.screens.home.preset + + +import android.content.res.Configuration +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.const.PulseDurations +import com.laseroptek.raman.const.handPieceTypes +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.model.Preset +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.ui.theme.robotoTextStyle +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.degreeToStep +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.launch +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PresetSavePopup( + mainViewModel: MainViewModel = hiltViewModel(), + onClick: (Boolean) -> Unit = {}, + title: String = stringResource(R.string.save_title), +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current // context For open system setting + + val presetList by mainViewModel.presetList.collectAsState() + + // handPiece + val handPiece by mainViewModel.handPiece.collectAsState() + val handPieceTitle = handPieceTypes.get(handPiece.type) + + // pulseWidth + val pulseAngle by mainViewModel.pulseAngle.collectAsState() + val pulseStep = pulseAngle.degreeToStep(totalSteps = PulseDurations.size) + val pulseWidth = PulseDurations[pulseStep] + + // fluence + val fluenceAngle by mainViewModel.fluenceAngle.collectAsState() + val fluenceList by mainViewModel.fluenceList.collectAsState() + val fluenceStep = fluenceAngle.degreeToStep(totalSteps = fluenceList.size) + val fluence = fluenceList[fluenceStep] + + // repetition + val repetitionAngle by mainViewModel.repetitionAngle.collectAsState() + val repetitionList by mainViewModel.repetitionList.collectAsState() + val repetitionStep = repetitionAngle.degreeToStep(totalSteps = repetitionList.size) + val repetition = repetitionList[repetitionStep] + + // preset index (priority) + var selectedPriority by remember { mutableStateOf(0) } + + // preset name + var presetName by remember { mutableStateOf("") } + + /* + var toastMessage by remember { mutableStateOf("") } + if (toastMessage.isNotEmpty()) { + LaunchedEffect(toastMessage) { + delay(2500) // Show for 2.5 seconds + toastMessage = "" // Clear the message to hide it + } + } + */ + + // keyboard + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + val performOkClick = { + when { + // Condition 0: Check if the preset priority is selected. + /* + selectedPriority == 0 -> { + Timber.d("isEmpty - $selectedPriority") + Toast.makeText( + context, + "Preset priority is not selected. Please select it first.", + Toast.LENGTH_LONG + ).show() + } + */ + + // Condition 1: Check if the preset name is empty. + presetName.trim().isEmpty() -> { + Timber.d("isEmpty - $presetName") + Toast.makeText( + context, + "Preset name is empty. Please input it first.", + Toast.LENGTH_LONG + ).show() + //toastMessage = "Preset name is empty. Please input name first." + } + + // Condition 2: Check if the name already exists in the preset list. + // This removes the need for the 'existingPresetNames' variable. + presetList.any { it.name.trim() == presetName.trim() } -> { + Timber.d("hasName - $presetName") + Toast.makeText( + context, + "Selected name already exists. Please change name.", + Toast.LENGTH_LONG + ).show() + //toastMessage = "Selected name already exists. Please change name." + } + + // Condition 3 (Success): If all checks pass, proceed with saving. + else -> { + // Hide the keyboard first for a clean UI transition + focusManager.clearFocus() + + // 1. Get the current list of presets. + var listToModify = presetList + + // 2. Find and remove the old preset that occupies the same priority and handpiece slot. + val existingPresetToReplace = listToModify.find { preset -> + preset.priority == selectedPriority && preset.handPieceType == handPiece.type + } + + // 3. If a preset to replace was found, update it's priority to 0 from our temporary list. + if (existingPresetToReplace != null) { + Timber.d("Found existing preset to replace: $existingPresetToReplace. Removing it.") + //listToModify = listToModify.minus(existingPresetToReplace) + listToModify = listToModify.map { preset -> + if (preset.id == existingPresetToReplace.id) { + preset.copy(priority = 0) + } else { + preset + } + } + } + + // 4. Create the new preset object to be added. + val newPreset = Preset( + handPieceType = handPiece.type, + pulseWidth = pulseWidth, + fluence = fluence, + repetition = repetition, + name = presetName, + priority = selectedPriority, + id = (presetList.maxOfOrNull { it.id } ?: -1) + 1 // Ensure a unique ID + ) + Timber.d("New preset to be added: $newPreset") + + // 5. Add the new preset to the modified list. + val finalList = listToModify.plus(newPreset) + + // 6. Update the ViewModel with the final list. + mainViewModel.setPresetList(finalList) + + // 7. Save the updated list to preferences. + scope.launch { + mainViewModel.savePresetListToPreference() + } + + Timber.d("Final presetList: ${mainViewModel.presetList.value}") + + onClick(true) + } + } + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Added force = true + } + } + + FullScreen(contentAlignment= Alignment.TopCenter) { + Box { + Column( + modifier = Modifier + .size(633.px.dp, 370.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + //Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(50.px.dp)) + + Column( + modifier = Modifier + .size(633.px.dp, 270.px.dp) + .clip(RoundedCornerShape(16.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(16.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ) + .padding(start = 10.px.dp, end = 10.px.dp, top = 20.px.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // Table + Row( + modifier = Modifier + .size(585.px.dp, 78.px.dp) + .clip(RoundedCornerShape(4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ) + .border( + width = 1.4.px.dp, + //color = Color.White, + brush = Brush.linearGradient( + colors = listOf( + Color(183, 142, 9), + Color(75, 63, 23) + ), + //start = Offset(0f, 0f), // Optional: Start at the top-left + //end = Offset(200f, 200f) // Optional: End at the bottom-right + ), + shape = RoundedCornerShape(4.px.dp), + ) + .dropShadow( + shape = RoundedCornerShape(4.px.dp), + color = Color.Black.copy(alpha = 0.12f), + blur = 10.px.dp, + offsetX = 0.px.dp, + offsetY = 2.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Spotsize (HandPiece) + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topStart = 4.px.dp, bottomStart = 4.px.dp)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Spotsize (HandPiece) - Title + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topStart = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Spotsize(mm)", + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + // // Spotsize (HandPiece) - Value + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(bottomStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = handPieceTitle, + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // Pulse Duration (ms) + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Pulse Duration - Title + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RectangleShape, + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Fluence(J/cm²)", + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + // Pulse Duration - Value + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = pulseWidth.toString(), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // Fluence + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Fluence - Title + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RoundedCornerShape(topEnd = 4.px.dp), + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Fluence(J/cm²)", + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + // Fluence - Value + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(bottomStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = fluence.toString(), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + + VerticalDivider( + color = Color(228,228,231), + thickness = 1.px.dp, + modifier = Modifier + .fillMaxHeight() + .width(1.px.dp) + ) + + // Repetition (Hz) + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(topEnd = 4.px.dp, bottomEnd = 4.px.dp)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Repetition - Title + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(bottomStart = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(47, 39, 14), + Color(34, 33, 31), + ) + ) + ) + .innerShadow( + shape = RectangleShape, + color = Color(255, 255, 255).copy(alpha = 0.15f), + blur = 4.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Repetition(Hz)", + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.White, + textAlign = TextAlign.Center + ) + } + + // Repetition - Value + Row( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .clip(RoundedCornerShape(bottomEnd = 4.px.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(255, 255, 255), + Color(248, 245, 238), + ) + ) + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = repetition.toString(), + style = RobotoTypography.bodyMedium, + fontSize = 14.px.sp, + lineHeight = 20.px.sp, + letterSpacing = 0.25.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + } + + Spacer(modifier = Modifier.height(20.px.dp)) + + // EditBox + Row( + modifier = Modifier + .size(585.px.dp, 56.px.dp) + .clip(RoundedCornerShape(4.px.dp)) + .background(Color(245, 245, 245)) + ) { + OutlinedTextField( + value = presetName, + onValueChange = { newText -> + presetName = newText + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, //KeyboardType.Text, + imeAction = ImeAction.Done, + //autoCorrect = false + ), + placeholder = { Text("Name") }, + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + Timber.d("onDone - Save") + performOkClick() + }), + textStyle = robotoTextStyle.copy( + fontSize = 18.px.sp, + color = Color.Black + ), + modifier = Modifier + .fillMaxSize() + .focusRequester(focusRequester), + ) + + LaunchedEffect(Unit) { + Timber.d("focusRequester.requestFocus()") + focusRequester.requestFocus() + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + } + + // Preset Buttons, Cancel & OK + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 10.px.dp, end = 10.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + // Preset Buttons + for (i in 1..5) { + PresetButton( + modifier = Modifier.size(33.px.dp), + fontSize = 18, + no = i, + selected = selectedPriority, + //enabled = true, + onClick = { + Timber.d("onClick PresetButton - $i") + // Toggle selection if select again + if (i == selectedPriority) { + selectedPriority = 0 + } else { + selectedPriority = i + } + } + ) + Spacer(Modifier.width(10.px.dp)) + } + + // Spacer + Spacer(modifier = Modifier.weight(1f)) + + // Cancel + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onClick - Cancel") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + + // Ok + TextButton( + modifier = Modifier + .size(68.px.dp, 40.px.dp), + onClick = { + Timber.d("onClick - Save") + performOkClick() + } + ) { + Text( + text = stringResource(R.string.save), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black + ) + } + } + + } + } + + /* + Spacer(modifier = Modifier.height(10.px.dp)) + + if (toastMessage.isNotEmpty()) { + Text( + text = toastMessage, + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.White + ) + } + */ + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewPresetSavePopup( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + PresetSavePopup( + mainViewModel = mainViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetViewModel.kt new file mode 100644 index 0000000..e7ca03f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/preset/PresetViewModel.kt @@ -0,0 +1,452 @@ +package com.laseroptek.raman.ui.screens.home.preset + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.laseroptek.raman.const.EnergyTable_10_10 +import com.laseroptek.raman.const.EnergyTable_12_12 +import com.laseroptek.raman.const.EnergyTable_3_15 +import com.laseroptek.raman.const.EnergyTable_5_5 +import com.laseroptek.raman.const.EnergyTable_7_7 +import com.laseroptek.raman.const.Fluences +import com.laseroptek.raman.const.HzTable_10_10 +import com.laseroptek.raman.const.HzTable_12_12 +import com.laseroptek.raman.const.HzTable_3_15 +import com.laseroptek.raman.const.HzTable_5_5 +import com.laseroptek.raman.const.HzTable_7_7 +import com.laseroptek.raman.const.KEY_YELLOW +import com.laseroptek.raman.const.PresetList +import com.laseroptek.raman.const.PulseDurations +import com.laseroptek.raman.const.RepetitionsByColorKey +import com.laseroptek.raman.const.UpDownState +import com.laseroptek.raman.data.model.Preset +import com.laseroptek.raman.utils.ext.getKey2ListForKey1 +import com.laseroptek.raman.utils.ext.getValue +import com.laseroptek.raman.utils.ext.toFluenceMutableStateFlowMap +import com.laseroptek.raman.utils.ext.toMutableStateFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class PresetViewModel @Inject constructor( +) : ViewModel() { + + // energyTable - (PulseWidth(ms), Fluence(J/cm²)) -> Energy(J) + // - Handpiece 타입에 따라 각각의 Energy Table로 구분 + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 energyTable 업데이트 + // - 앱 실행시 저장된 테이블에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값) + private val _energyTable: MutableStateFlow, Float>> = + EnergyTable_5_5.toFluenceMutableStateFlowMap() + val energyTable = _energyTable.asStateFlow() + fun setEnergyTable(e: MutableMap, Float>) { + _energyTable.value = e + } + + // fluenceList - handpiece type에 따른 Fluence 단계 값 (Guage 단계값 테이블) + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 energyTable 업데이트 + // - Fluences는 5_5 일때의 목록 값 (초기값: handPeiece none 상태) + // - ref. Repetitions, PulseDurations는 fixed Table 임 + private val _fluenceList: MutableStateFlow> = + Fluences.toMutableStateFlow() + val fluenceList = _fluenceList.asStateFlow() + fun setFluenceList(f: List) { + viewModelScope.launch { + _fluenceList.value = f + } + } + + // hzTable - (PulseWidth(ms), Fluence(J/cm²)) -> hzType(int) + // - Handpiece 타입에 따라 각각의 Energy Table로 구분 + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 hzTable 업데이트 + // - 앱 실행시 저장된 테이블에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값) + private val _hzTable: MutableStateFlow, Int>> = + HzTable_5_5.toFluenceMutableStateFlowMap() + val hzTable = _hzTable.asStateFlow() + fun setHzTable(h: MutableMap, Int>) { + _hzTable.value = h + } + + // repetitionList - handpiece type에 따른 Repetition 단계 값 (Guage 단계값 테이블) + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 hzTable 업데이트 + // - Repetition은 5_5 일때의 목록 값 (초기값: handPeiece none 상태) + // - ref. PulseDurations는 fixed Table 임 + private val _repetitionList: MutableStateFlow> = + RepetitionsByColorKey[KEY_YELLOW]!!.toMutableStateFlow() + val repetitionList = _repetitionList.asStateFlow() + fun setRepetitionList(r: List) { + _repetitionList.value = r + } + + fun loadHzTable(handPieceType: Int) { + Timber.d("loadHzTable($handPieceType)") + + val hzTable = when (handPieceType) { + 2 -> HzTable_7_7 + 3 -> HzTable_10_10 + 4 -> HzTable_12_12 + 5 -> HzTable_3_15 + else -> HzTable_5_5 + } + + setHzTable( hzTable.mapValues { (_, fValue) -> fValue }.toMutableMap() ) + } + + fun loadFluenceTable(handPieceType: Int) { + Timber.d("loadFluenceTable($handPieceType)") + + val energyTable = when (handPieceType) { + 2 -> EnergyTable_7_7 + 3 -> EnergyTable_10_10 + 4 -> EnergyTable_12_12 + 5 -> EnergyTable_3_15 + else -> EnergyTable_5_5 + } + + setEnergyTable( energyTable.mapValues { (_, fValue) -> fValue }.toMutableMap() ) + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + + private val _selectedRowIndex: MutableStateFlow = MutableStateFlow(0) + val selectedRowIndex = _selectedRowIndex.asStateFlow() + fun setSelectedRowIndex(i: Int) { _selectedRowIndex.value = i } + + private val _isEditMode: MutableStateFlow = MutableStateFlow(false) + val isEditMode = _isEditMode.asStateFlow() + fun setIsEditMode(e: Boolean) { _isEditMode.value = e } + //fun toggleIsEditMode() { _isEditMode.value = !_isEditMode.value } + + // copied from mainViewModel + // selectedPresetIndex - + private val _selectedPresetIndex: MutableStateFlow = MutableStateFlow(0) + val selectedPresetIndex = _selectedPresetIndex.asStateFlow() + fun setSelectedPresetIndex(i: Int) { _selectedPresetIndex.value = i } + + // Define the custom comparator + private val presetComparator = Comparator { a, b -> + val aIsZero = a.priority == 0 + val bIsZero = b.priority == 0 + + when { // If a's priority is 0 and b's is not, a comes after b. + aIsZero && !bIsZero -> 1 + // If b's priority is 0 and a's is not, a comes before b. + !aIsZero && bIsZero -> -1 + // Otherwise (both are 0 or both are non-zero), sort by priority, + // then by name as a secondary sort. + else -> { + val priorityCompare = a.priority.compareTo(b.priority) + if (priorityCompare != 0) { + priorityCompare + } else { + a.name.compareTo(b.name) + } + } + } + } + + // copied from mainViewModel + // presetList - Preset 목록 + // - Preset Save Popup & Load Popup 에서 목록 변경 + // - 앱 실행시 저장된 목록을 preference 에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값) + private val _presetList = MutableStateFlow>( + // Initialize with a deep copy of PresetList, sorted as per your original logic + PresetList.map { it.copy() } + .sortedWith(presetComparator) + ) + val presetList = _presetList + fun setPresetList(list: List = PresetList) { + // deep copy + _presetList.value = list + .map{ it.copy() } + } + fun setPresetListSorted(list: List = PresetList) { + // deep copy + _presetList.value = list + .map{ it.copy() } + .sortedWith(presetComparator) + } + fun clearPreset() { + setPresetList( emptyList() ) + } + fun deletePreset(index: Int) { + val currentList = _presetList.value.toMutableList() + if (index >= 0 && index < currentList.size) { + currentList.removeAt(index) + //_presetList.value = currentList // Assign new list to StateFlow + setPresetList(currentList) + } else { + Timber.e("deletePreset: Invalid index $index for list size ${currentList.size}") + } + } + fun getPreset(index: Int): Preset? { + var preset:Preset? = null + val currentList = _presetList.value.toMutableList() + if (index >= 0 && index < currentList.size) { + preset = currentList.get(index) + } else { + Timber.e("deletePreset: Invalid index $index for list size ${currentList.size}") + } + return preset + } + fun setPreset(newPreset: Preset) { + // 1. create a new list with the updated item - immutable way to update a StateFlow>. + val updatedList = _presetList.value.map { localPreset -> + if (localPreset.id == newPreset.id) { + // This is the preset to update. Replace it with the master version. + Timber.d("Refreshing preset (id: ${newPreset.id}, name: '${newPreset.name}').") + newPreset.copy() // Use a copy to be safe + } else { + // This is not the preset we're looking for, so return it unchanged. + localPreset + } + } + + // 2. check if the list was actually changed before emitting a new value. + if (_presetList.value != updatedList) { + // 3. Update the local StateFlow with the new list. + _presetList.value = updatedList + } + } + fun addPreset(preset: Preset) { + val currentList = _presetList.value.toMutableList() + currentList.add(preset.copy()) // Add a copy to ensure immutability if preset is reused + // Re-sort if adding can change order, or decide on insertion logic + _presetList.value = currentList.sortedWith(presetComparator) + } + fun updatePresetName(index: Int, newName: String) { + val currentList = _presetList.value.toMutableList() + if (index >= 0 && index < currentList.size) { + val updatedPreset = currentList[index].copy(name = newName) + currentList[index] = updatedPreset + //_presetList.value = currentList // Assign new list to StateFlow + setPresetList(currentList) + Timber.d("Updated preset at index $index with name: $newName. List: ${_presetList.value}") + } else { + Timber.e("updatePresetName: Invalid index $index for list size ${currentList.size}") + } + } + + /** 선택한 handPieceType의 초기값으로 pulseWidth, fluence, repetition 설정) */ + fun updateHandPieceType(index: Int, handPieceType: Int, isUp: UpDownState) { + val currentList = _presetList.value.toMutableList() // Get current list from StateFlow + if (index >= 0 && index < currentList.size) { + val no_of_hand_piece = 5 + // 1. handPienceType Update + val newHandPieceType = if (isUp == UpDownState.Up) { + if (handPieceType < no_of_hand_piece -1) handPieceType + 1 else handPieceType + } else { + if (handPieceType > 0) handPieceType -1 else handPieceType + } + + // 2. load tables + loadFluenceTable(newHandPieceType) + loadHzTable(newHandPieceType) + + // 3. get pulseWidth with index 0 + val newPulseWidthIndex = 0 + val newPulseWidth = PulseDurations[newPulseWidthIndex] + + // 4. fluence update (energyTable) + val newFluenceList = energyTable.value.getKey2ListForKey1(newPulseWidth) + val newFluence = newFluenceList.get(0) + + // 5. repetition update (hzTable) + val hzType = hzTable.value.getValue(newPulseWidth, newFluence) + Timber.d("hzType: ${hzType}") + val list = RepetitionsByColorKey.get(hzType)!! + setRepetitionList( list ) + + val newRepetitionIndex = 0 + val newRepetition = list[newRepetitionIndex] + + val updatedPreset = currentList[index].copy( + handPieceType = newHandPieceType, + pulseWidth = newPulseWidth, + fluence = newFluence, + repetition = newRepetition + ) + currentList[index] = updatedPreset + // Instead of calling setPresetList (which re-sorts and deep copies everything again), + // directly update the value for better performance if sorting isn't needed here. + //_presetList.value = currentList // Assign new list to StateFlow + setPresetList(currentList) + Timber.d("Updated handPieceType at index $index with handPieceType: $newHandPieceType.") + + // get index of updatedPreset + // The sorted list is now the current value of _presetList + val newIndex = _presetList.value.indexOf(updatedPreset) + if (newIndex != -1) { + Timber.d("Found updatedPreset at new index: $newIndex") + // Now you can use this newIndex, for example, to update the selected row + setSelectedPresetIndex(newIndex) + } else { + Timber.e("Could not find the updated preset in the list after sorting.") + } + } else { + Timber.e("updateHandPieceType: Invalid index $index for list size ${currentList.size}") + } + } + + fun updatePulseWidth(index: Int, handPieceType: Int, pulseWidth: Float, isUp: UpDownState) { + val currentList = _presetList.value.toMutableList() + if (index < 0 || index >= currentList.size) { + Timber.e("updatePulseWidth: Invalid index $index for list size ${currentList.size}") + return + } + + // 1. load tables + loadFluenceTable(handPieceType) + loadHzTable(handPieceType) + + // 2. update pulseWidth + val pulseWidthIndex = PulseDurations.indexOf(pulseWidth).takeIf { it != -1 } ?: 0 + val newPulseWidth = if (isUp == UpDownState.Up) { + if (pulseWidthIndex < PulseDurations.size - 1) PulseDurations[pulseWidthIndex + 1] else pulseWidth + } else { + if (pulseWidthIndex > 0) PulseDurations[pulseWidthIndex - 1] else pulseWidth + } + + // 3. update fluence + val newFluenceList = energyTable.value.getKey2ListForKey1(newPulseWidth) + val newFluenceIndex = 0 + val newFluence = newFluenceList.get(newFluenceIndex) + + // 4. update repetition + val hzType = hzTable.value.getValue(newPulseWidth, newFluence) + val list = RepetitionsByColorKey[hzType]!! + setRepetitionList( list ) + + val newRepetitionIndex = 0 + val newRepetition = repetitionList.value[newRepetitionIndex] + + val updatedPreset = currentList[index].copy( + pulseWidth = newPulseWidth, + fluence = newFluence, + repetition = newRepetition + ) + currentList[index] = updatedPreset + //_presetList.value = currentList + setPresetList(currentList) + Timber.d("Updated pulseWidth at index $index with newPulseWidth: $newPulseWidth.") + } + + fun updateFluence(index: Int, handPieceType: Int, pulseWidth: Float, fluence: Float, isUp: UpDownState) { + val currentList = _presetList.value.toMutableList() + if (index < 0 || index >= currentList.size) { + Timber.e("updateFluence: Invalid index $index for list size ${currentList.size}") + return + } + + // 1. load tables + loadFluenceTable(handPieceType) + loadHzTable(handPieceType) + + // 2. get fluence + val newFluenceList = energyTable.value.getKey2ListForKey1(pulseWidth) + val newFluenceIndex = newFluenceList.indexOf(fluence).takeIf { it != -1 } ?: 0 + val newFluence = if (isUp == UpDownState.Up) { + if (newFluenceIndex < newFluenceList.size - 1) newFluenceList[newFluenceIndex + 1] else fluence + } else { + if (newFluenceIndex > 0) newFluenceList[newFluenceIndex - 1] else fluence + } + + // 3. update repetition (0) + val hzType = hzTable.value.getValue(pulseWidth, newFluence) + val list = RepetitionsByColorKey[hzType]!! + setRepetitionList( list ) + + val newRepetitionIndex = 0 + val newRepetition = repetitionList.value[newRepetitionIndex] + + // The original code here had a bug: it was updating `pulseWidth = newFluence` + // It should be `fluence = newFluence` + val updatedPreset = currentList[index].copy( + fluence = newFluence, + repetition = newRepetition + ) + currentList[index] = updatedPreset + //_presetList.value = currentList + setPresetList(currentList) + Timber.d("Updated fluence at index $index with newFluence: $newFluence. FluenceList: $fluenceList") + } + + fun updateRepetition(index: Int, handPieceType: Int, pulseWidth: Float, fluence: Float, repetition: Float, isUp: UpDownState) { + val currentList = _presetList.value.toMutableList() + if (index < 0 || index >= currentList.size) { + Timber.e("updateRepetition: Invalid index $index for list size ${currentList.size}") + return + } + + // 1. load tables + loadFluenceTable(handPieceType) + loadHzTable(handPieceType) + + // update repetition + val hzType = hzTable.value.getValue(pulseWidth, fluence) + val list = RepetitionsByColorKey[hzType]!! + setRepetitionList( list ) + + val newRepetitionIndex = repetitionList.value.indexOf(repetition).takeIf { it != -1 } ?: 0 + val newRepetition = if (isUp == UpDownState.Up) { + // Original logic: repetitionIndex < PulseDurations.size -1 (Likely a typo, should be Repetitions.size) + if (newRepetitionIndex < repetitionList.value.size - 1) repetitionList.value[newRepetitionIndex + 1] else repetition + } else { + // Original logic: repetitionIndex > 1 + if (newRepetitionIndex > 0) repetitionList.value[newRepetitionIndex - 1] else repetition + } + + val updatedPreset = currentList[index].copy(repetition = newRepetition) + currentList[index] = updatedPreset + //_presetList.value = currentList + setPresetList(currentList) + Timber.d("Updated repetition at index $index with newRepetition: $newRepetition.") + } + + fun updatePriority(index: Int, priority: Int, isUp: UpDownState) { + val currentList = _presetList.value.toMutableList() + if (index < 0 || index >= currentList.size) { + Timber.e("updateRepetition: Invalid index $index for list size ${currentList.size}") + return + } + + val maxPriority = 5 // Assuming 5 is the max assignable priority + val newPriority = if (isUp == UpDownState.Up) { + if (priority == 0) 1 else (priority + 1).coerceAtMost(maxPriority) + } else { + if (priority == 1) 0 else (priority - 1).coerceAtLeast(0) + } + + // update conflicting priority to 0 + /* + val curHandPieceType = currentList[index].handPieceType + currentList.filter{ it.handPieceType == curHandPieceType && it.priority == newPriority }.forEach { + it.priority = 0 + } + */ + + val updatedPreset = currentList[index].copy(priority = newPriority) + currentList[index] = updatedPreset + //_presetList.value = currentList + setPresetList(currentList) + Timber.d("Updated priority at index $index with newPriority: $newPriority.") + + // get index of updatedPreset + // The sorted list is now the current value of _presetList + val newIndex = _presetList.value.indexOf(updatedPreset) + + if (newIndex != -1) { + Timber.d("Found updatedPreset at new index: $newIndex") + // Now you can use this newIndex, for example, to update the selected row + setSelectedPresetIndex(newIndex) + } else { + Timber.e("Could not find the updated preset in the list after sorting.") + } + } + + init { + Timber.d("PresetViewModel init") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/CircularSlider.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/CircularSlider.kt new file mode 100644 index 0000000..c14b46f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/CircularSlider.kt @@ -0,0 +1,275 @@ +package com.laseroptek.raman.ui.screens.home.slider + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.util.Log +import android.view.MotionEvent +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +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.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round +import com.laseroptek.raman.R +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber +import kotlin.math.PI +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin +import kotlin.math.sqrt + +@SuppressLint("NewApi") +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun CircularSlider( + modifier: Modifier = Modifier.Companion, + angleNew: Float = 0f, + padding: Float = 10f, + stroke: Float = 30f, + touchStroke: Float = 60f, + cap: StrokeCap = StrokeCap.Round, + onChange: ((Float) -> Unit)? = null +) { + Timber.d("angleNew: ${angleNew}") + + var width by remember { mutableStateOf(0) } + var height by remember { mutableStateOf(0) } + var angleOld by remember { mutableStateOf( angleNew ) } + var nearTheThumbIndicator by remember { + mutableStateOf( + false + ) + } + var radius by remember { mutableStateOf(10f) } + var center by remember { + mutableStateOf( + Offset.Zero + ) + } + var appliedAngle by remember { + mutableStateOf( + 0f + ) + } + val trackColor: Color = Color(212,212,212) + val trackBorderColor: Color = Color(164,164,164) + val gradientBorderColor: Color = Color(100,0,100) + + //Timber.d("radius: ${radius}, center: ${center}") + //Timber.d("angleNew.value: ${angleNew.value}, angleOld: ${angleOld}") + + LaunchedEffect(key1 = angleOld) { + Timber.d(">> angleOld: %f", angleOld) + + if (angleOld > 270.0f && angleOld < 315.0f) { + angleOld = 270.0f + } else if (angleOld >= 315.0f && angleOld < 360f) { + angleOld = 0.0f + } else if (angleOld >= 360f) { + angleOld = 0f + } + + appliedAngle = angleOld + + Timber.d(">> appliedAngle: %f", appliedAngle) + onChange?.invoke(appliedAngle) + } + + appliedAngle = angleNew + val sweepAngle = -(270.0f - appliedAngle) + + Timber.d("## appliedAngle: $appliedAngle, sweepAngle: $sweepAngle") + val gradient = Brush.sweepGradient( + colorStops = arrayOf( + 0.0f to Color(216,33,9), + 0.1f to Color(216,33,9), + 0.3f to Color(250,250,3), + 0.4f to Color(255,245,0), + 1.0f to Color(216,33,9), + ), + center = Offset(width / 2f, height / 2f) + ) + + + + val imageBitMap = ImageBitmap.imageResource( + id = R.drawable.ic_btn_control + ) + + + Canvas( + modifier = modifier + .onGloballyPositioned { + width = it.size.width + height = it.size.height + center = Offset(width / 2f, height / 2f) + radius = min(width.toFloat(), height.toFloat()) / 2f - padding - stroke / 2f + } + .pointerInteropFilter { + val x = it.x + val y = it.y + val offset = Offset(x, y) + when (it.action) { + MotionEvent.ACTION_DOWN -> { + val d = distance(offset, center) + val a = angle(center, offset) + if (d >= radius - touchStroke / 2f && d <= radius + touchStroke / 2f) { + nearTheThumbIndicator = true + if (a >= 0 && a <= 270) { + Timber.d("ACTION_DOWN: angle: %f", a) + angleOld = a + } + } else { + nearTheThumbIndicator = false + } + } + + MotionEvent.ACTION_MOVE -> { + if (nearTheThumbIndicator) { + val angleMove = angle(center, offset) + //if (angleMove >=0 && angleMove <= 270) { + Timber.d("ACTION_MOVE: angle: %f", angleMove) + angleOld = angleMove + //} + } + } + + MotionEvent.ACTION_UP -> { + Timber.d("ACTION_UP") + + nearTheThumbIndicator = false + } + + else -> return@pointerInteropFilter false + } + return@pointerInteropFilter true + } + ) { + //Timber.d("appliedAngle: $appliedAngle, sweepAngle: $sweepAngle") + + // background border + drawArc( + color = trackBorderColor, + startAngle = 45f, + sweepAngle = -270f, + topLeft = center - Offset(radius, radius), + size = Size(radius * 2, radius * 2), + useCenter = false, + style = Stroke( + width = stroke + 0.1f, + cap = cap + ) + ) + + // background + drawArc( + color = trackColor, + startAngle = 45f, + sweepAngle = -270f, + topLeft = center - Offset(radius, radius), + size = Size(radius * 2, radius * 2), + useCenter = false, + style = Stroke( + width = stroke, + cap = cap + ) + ) + + // background + drawArc( + color = gradientBorderColor, + startAngle = 135f, + sweepAngle = appliedAngle, + topLeft = center - Offset(radius, radius), + size = Size(radius * 2, radius * 2), + useCenter = false, + style = Stroke( + width = stroke + 0.1f, + cap = cap + ), + ) + + drawArc( + brush = gradient, + startAngle = 135f, + sweepAngle = appliedAngle, + topLeft = center - Offset(radius, radius), + size = Size(radius * 2, radius * 2), + useCenter = false, + style = Stroke( + width = stroke, + cap = cap + ), + ) + + drawImage( + image = imageBitMap, + dstOffset = Offset( + (center.x - padding - stroke) + (radius) * cos( (appliedAngle + 135.0f) * PI / 180.0 ).toFloat(), + (center.y - padding/2 - stroke) + (radius) * sin( (appliedAngle + 135.0f) * PI / 180.0 ).toFloat() + ).round(), + dstSize = IntSize((stroke * 3).toInt(), (stroke * 3).toInt()), + ) + } +} + +// 135.0도를 시작점 0 도로 계산 ~ 270도 까지 +fun angle(center: Offset, offset: Offset, startAngle: Float = 135.0f): Float { + val rad = atan2(offset.y - center.y, offset.x - center.x) + var deg = Math.toDegrees(rad.toDouble()) + deg = (deg + 360) % 360 + var deg2 = deg - startAngle + deg2 = (deg2 + 360) % 360 + + Timber.d("org: $deg, new: $deg2") + return deg2.toFloat() +} + +fun distance(first: Offset, second: Offset): Float { + return sqrt((first.x - second.x).square() + (first.y - second.y).square()) +} + +fun Float.square(): Float { + return this * this +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1080dp,height=720dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun preViewCircularSlider() { + CircularSlider( + modifier = Modifier.size(400.px.dp), + ) { + Log.d("OnChange: Original VAL", "${it * 180}") + var sliderPercentage = it + var currTempValue = 16 + (sliderPercentage * 16) + var roundedTempValue = "%.0f".format(currTempValue) + //mainViewModel.tempValue.value = roundedTempValue + Log.d("progress", "$roundedTempValue°C") + + //onChange?.invoke(it * 180f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/LaserControlView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/LaserControlView.kt new file mode 100644 index 0000000..003e3de --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/LaserControlView.kt @@ -0,0 +1,80 @@ +package com.laseroptek.raman.ui.screens.home.slider + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.const.Fluences +import com.laseroptek.raman.const.LaserStatusType +import com.laseroptek.raman.const.UpDownState +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + + +@Composable +fun LaserControlView( + modifier: Modifier = Modifier, + //mainViewModel: MainViewModel = hiltViewModel(), + fluenceList: List = Fluences, + repetitionList: List = listOf(1f), + pulseType: Int = 1, + handPieceType: Int = 0, + type: LaserStatusType = LaserStatusType.PULSE_DURATION, + angle: Float = 0f, + onChange: (Float) -> Unit = {}, + onClick: (UpDownState) -> Unit = {}, +) { + Timber.d("angle: ${angle}") + + Box(modifier = modifier) { + SliderBox( + //mainViewModel = mainViewModel, + fluenceList = fluenceList, + repetitionList = repetitionList, + pulseType = pulseType, + handPieceType = handPieceType, + type = type, + angle = angle, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(Color.Transparent) + .align(Alignment.TopCenter), + onChange = onChange + ) + + UpDownButton( + modifier = Modifier + .size(190.px.dp, 42.px.dp) + .background(color = Color.Transparent) + .align(Alignment.BottomCenter), + onClick = onClick, + ) + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + //device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewLaserControlView( +) { + LaserControlView( + modifier = Modifier + .size(330.px.dp, 380.px.dp), + type = LaserStatusType.REPETITION, + //mainViewModel = mainViewModel + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/SlideBox.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/SlideBox.kt new file mode 100644 index 0000000..f7415df --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/SlideBox.kt @@ -0,0 +1,293 @@ +package com.laseroptek.raman.ui.screens.home.slider + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.laseroptek.raman.R +import com.laseroptek.raman.const.Fluences +import com.laseroptek.raman.const.LaserStatusType +import com.laseroptek.raman.const.PulseDurations +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.degreeToStep +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber + + +@Composable +fun SliderBox( + modifier: Modifier = Modifier, + fluenceList: List = Fluences, + repetitionList: List = listOf(1f), + pulseType: Int = 1, + type : LaserStatusType = LaserStatusType.PULSE_DURATION, + handPieceType: Int = 0, + angle: Float = 0f, + padding : Float = 10f, + stroke : Float = 30f, + touchStroke : Float = 60f, + onChange: ((Float) -> Unit)? = null, +) { + Timber.d("type: ${type}") + Timber.d("fluenceList: ${fluenceList}") + Timber.d("repetitionList: ${repetitionList}") + + // Center Text & Below + // + val txtValue = when(type) { + LaserStatusType.PULSE_DURATION -> { + val pulseDuration = PulseDurations[angle.degreeToStep(totalSteps = PulseDurations.size)] + Timber.d("PULSE_DURATION -> angle: ${angle}") + if (pulseDuration - pulseDuration.toInt() == 0f) { + "%d".format(pulseDuration.toInt()) + } else { + "%.1f".format(pulseDuration) + } + + } + LaserStatusType.FLUENCE -> { + val fluence = fluenceList[angle.degreeToStep(totalSteps = fluenceList.size)] + Timber.d("FLUENCE -> angle: ${angle}") + if (fluence - fluence.toInt() == 0f) { + "%d".format(fluence.toInt()) + } else { + "%.1f".format(fluence) + } + } + LaserStatusType.REPETITION -> { + if (repetitionList.size < 1) { + return + } + + val repetion = if (pulseType == 0) { + // Single + 0.0f + } else if (repetitionList.size == 1) { + repetitionList[0] + } else { + // Repeate + repetitionList[angle.degreeToStep(totalSteps = repetitionList.size)] + } + + Timber.d("REPETITION -> angle: ${angle}") + if (repetion - repetion.toInt() == 0f) { + "%d".format(repetion.toInt()) + } else { + "%.1f".format(repetion) + } + } + else -> { "" } + } + val txtUnit = when(type) { + LaserStatusType.PULSE_DURATION -> { + "ms" + } + LaserStatusType.FLUENCE -> { + "J/cm²" + } + LaserStatusType.REPETITION -> { + "Hz" + } + else -> { "" } + } + val txtTitle = when(type) { + LaserStatusType.PULSE_DURATION -> { + "Pulse Duration" + } + LaserStatusType.FLUENCE -> { + "Fluence" + } + LaserStatusType.REPETITION -> { + "Repetition" + } + else -> { "" } + } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.Transparent), + contentAlignment = Alignment.TopCenter, + ) { + val controlImageId = when (type) { + LaserStatusType.FLUENCE -> { R.drawable.ic_control_fluence } + LaserStatusType.REPETITION -> { R.drawable.ic_control_repetition } + else -> { R.drawable.ic_control_duration } + } + + // Image 1 + Image( + painter = painterResource(id = controlImageId), + contentDescription = "Background Image - Indicator", + modifier = Modifier + .fillMaxSize() + .padding( + top = (padding + 5 + stroke).toDouble().px.dp, + start = (padding + stroke).toDouble().px.dp, + end = (padding + stroke).toDouble().px.dp, + bottom = (padding - 5 + stroke).toDouble().px.dp + ) + .zIndex(-1f), + contentScale = ContentScale.Crop + ) + + // Slider + CircularSlider( + modifier = Modifier + .fillMaxSize() + .padding( + (padding).toDouble().px.dp + ) + .zIndex(1f), + angleNew = angle, + padding = (padding), + stroke = stroke, + touchStroke = touchStroke, + onChange = { + Timber.d("onChange: Original VAL : ${it}") + onChange?.invoke(it) + } + ) + + // Value + Box( + Modifier + .size(110.px.dp) + .align(Alignment.Center) + .padding( + top = if (type == LaserStatusType.FLUENCE) 5.px.dp else 12.px.dp, + bottom = if (type == LaserStatusType.FLUENCE) 13.px.dp else 15.px.dp + ) + .zIndex(2f) + ) { + if (handPieceType == 0) { + Text( + text = "-", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + style = RobotoTypography.bodySmall.copy( + color = Color.White, + letterSpacing = 1.px.sp, + fontSize = 58.px.sp, + fontWeight = FontWeight.Thin + ) + ) + } else if (pulseType == 0 && type == LaserStatusType.REPETITION) { + Text( + text = "SIG", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + style = RobotoTypography.bodySmall.copy( + color = Color.White, + letterSpacing = 1.px.sp, + fontSize = 58.px.sp, + fontWeight = FontWeight.Thin + ) + ) + } else { + Text( + text = txtValue, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopCenter), + style = RobotoTypography.bodySmall.copy( + color = Color.White, + letterSpacing = if (txtValue.length > 2) -5.px.sp else 2.px.sp, + fontSize = if (type == LaserStatusType.FLUENCE) 64.px.sp else 58.px.sp, + fontWeight = FontWeight.Thin + ) + ) + Text( + text = txtUnit, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + style = RobotoTypography.bodySmall.copy( + fontSize = 16.px.sp, + color = Color.White, + fontWeight = FontWeight.Thin + ) + ) + } + } + + // Title + Column( + Modifier + .size(200.px.dp, 40.px.dp) + .align(Alignment.BottomCenter) + .zIndex(3f) + ) { + Text( + text = txtTitle, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + style = RobotoTypography.bodyMedium.copy( + color = Color.Black, + fontWeight = FontWeight.Thin, + fontSize = 20.px.sp, + lineHeight = 24.px.sp + ) + ) + Spacer(modifier = Modifier.weight(1f)) + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=460dp,height=460dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSliderBox( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + SliderBox( + //mainViewModel = mainViewModel + //type = LaserStatusType.FLUENCE, + type = LaserStatusType.REPETITION, + pulseType = 0, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/UpDownButton.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/UpDownButton.kt new file mode 100644 index 0000000..4903665 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/slider/UpDownButton.kt @@ -0,0 +1,254 @@ +package com.laseroptek.raman.ui.screens.home.slider + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.UpDownState +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.innerShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + + +@Composable +fun UpDownButton( + onClick: (UpDownState) -> Unit = {}, + modifier: Modifier = Modifier, +) { + val updatedOnClick by rememberUpdatedState(onClick) + val initialDelayMillis = 500L // Wait this long before starting to repeat + val repeatIntervalMillis = 100L // Repeat every 100ms + + val scope = rememberCoroutineScope() + + Row( + modifier = modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + // Left Button + Row( + modifier = Modifier + .graphicsLayer() + .clip(RoundedCornerShape(52.px.dp)) + .size(80.px.dp, 42.px.dp) + /* + .noRippleClickable { + onClick(UpDownState.Down) + } + */ + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + + // 1. Fire the first click immediately on press down. + updatedOnClick(UpDownState.Down) + + // 2. Start a repeating loop job using the scope from rememberCoroutineScope. + val pressJob: Job = scope.launch(Dispatchers.Default) { + // 3. Wait for the initial delay. + delay(initialDelayMillis) + // 4. Start the repeating loop. + while (true) { + updatedOnClick(UpDownState.Down) + delay(repeatIntervalMillis) + } + } + + // 5. Wait for the user to release their finger. + waitForUpOrCancellation() + + // 6. Cancel the repeating loop when the finger is released. + pressJob.cancel() + } + } + .border( + border = BorderStroke( + width = 3.dp, + brush = Brush.linearGradient( + colors = listOf(Color(81, 81, 81), Color(64, 64, 64)), + //start = Offset(0f, 0f), // Optional: Start at the top-left + //end = Offset(200f, 200f) // Optional: End at the bottom-right + ) + ), + shape = RoundedCornerShape(52.dp) + ) + + .innerShadow( + shape = RoundedCornerShape(52.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.14f), + blur = 7.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .background( + Color(225, 225, 225) + /* + brush = Brush.linearGradient( + colors = listOf( + Color(250, 250, 250), + Color(198, 198, 198), + ) + ) + */ + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_left), + contentDescription = null, + modifier = Modifier.size(32.px.dp, 32.px.dp), + contentScale = ContentScale.FillBounds + ) + } + + Spacer(Modifier.width(30.px.dp)) + + // Right Button + Row( + modifier = Modifier + .graphicsLayer() + .clip(RoundedCornerShape(52.px.dp)) + .size(80.px.dp, 42.px.dp) + /* + .noRippleClickable { + onClick(UpDownState.Up) + } + */ + .pointerInput(Unit) { + awaitEachGesture { + // Remove the unnecessary 'awaitPointerEventScope' wrapper + // The 'awaitEachGesture' block provides the context we need. + + // Wait for the initial press up + val up = awaitFirstDown(requireUnconsumed = false) + + // 1. Fire the first click immediately on press down. + updatedOnClick(UpDownState.Up) + + // 2. Start a repeating loop job using the scope from rememberCoroutineScope. + val pressJob: Job = scope.launch(Dispatchers.Default) { + // 3. Wait for the initial delay. + delay(initialDelayMillis) + // 4. Start the repeating loop. + while (true) { + updatedOnClick(UpDownState.Up) + delay(repeatIntervalMillis) + } + } + + // 5. Wait for the user to release their finger. + waitForUpOrCancellation() + + // 6. Cancel the repeating loop when the finger is released. + pressJob.cancel() + } + } + .border( + border = BorderStroke( + width = 3.dp, + brush = Brush.linearGradient( + colors = listOf(Color(81, 81, 81), Color(64, 64, 64)), + //start = Offset(0f, 0f), // Optional: Start at the top-left + //end = Offset(200f, 200f) // Optional: End at the bottom-right + ) + ), + shape = RoundedCornerShape(52.dp) + ) + .dropShadow( + shape = RoundedCornerShape(52.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.14f), + blur = 7.px.dp, + offsetY = 5.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .innerShadow( + shape = RoundedCornerShape(52.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.14f), + blur = 7.px.dp, + offsetY = 4.px.dp, + offsetX = 0.px.dp, + spread = 0.px.dp + ) + .background( + Color(225, 225, 225) + /* + brush = Brush.linearGradient( + colors = listOf( + Color(198, 198, 198), + Color(250, 250, 250), + ) + ) + */ + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_right), + contentDescription = null, + modifier = Modifier.size(32.px.dp, 32.px.dp), + contentScale = ContentScale.FillBounds + ) + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + //device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewUpDownButton( + /* + mainViewModel:MainViewModel = MainViewModel( + chartDataRepository = ChartDataRepository(), + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) + */ +) { + UpDownButton( + //mainViewModel = mainViewModel, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/standby/StandByButton.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/standby/StandByButton.kt new file mode 100644 index 0000000..e9e9e50 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/standby/StandByButton.kt @@ -0,0 +1,254 @@ +package com.laseroptek.raman.ui.screens.home.standby + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + + +@Composable +fun StandByButton( + modifier: Modifier = Modifier, + laserStatus: Int = 0x53, + //mainViewModel: MainViewModel = hiltViewModel(), + onClick: () -> Unit = {}, +) { + //val laserStatus by mainViewModel.laserStatus.collectAsState() + //val handPiece by mainViewModel.handPiece.collectAsState() + + // 0x53: standBy (default) + val buttonText = when (laserStatus) { + 0x52 -> stringResource(id = R.string.ready) + 0x49, 0x4C -> stringResource(id = R.string.laseron) + else -> stringResource(id = R.string.standby) + } + + val iconResource = when (laserStatus) { + 0x52 -> R.drawable.ic_ready + 0x49, 0x4C -> R.drawable.ic_laseron + else -> R.drawable.ic_standby + } + + val textBrush = when (laserStatus) { + 0x52 -> { + SolidColor(Color(0, 0, 0)) + } + 0x49, 0x4C -> { + SolidColor(Color(255, 255, 255)) + } + else -> { + SolidColor(Color(248,193,15)) + } + /* + else -> { + Brush.linearGradient( + colors = listOf( + Color(248,193,15), + Color(255,245,212), + Color(251,225,137) + ), + start = Offset(0f, 50f), + end = Offset(100f, 50f) + ) + } + */ + } + + val backgroundBrush = when (laserStatus) { + 0x52 -> { + SolidColor(Color(243, 199, 54)) + /* + Brush.linearGradient( + colors = listOf( + Color(29,78,216), + Color(30,58,138), + ), + start = Offset(0f, 50f), + end = Offset(100f, 50f) + ) + */ + } + 0x49, 0x4C -> { + Brush.linearGradient( + colors = listOf( + Color(163,17,17), + Color(204,20,20), + Color(112,9,9), + ), + start = Offset(0f, 50f), + end = Offset(100f, 50f) + ) + } + else -> { + Brush.linearGradient( + colors = listOf( + Color(43,37,18), + Color(23,23,22), + ), + start = Offset(0f, 50f), + end = Offset(100f, 50f) + ) + } + } + + val borderBrush = when (laserStatus) { + 0x52 -> { + Brush.linearGradient( + colors = listOf( + Color(222,216,185), + Color(255,255,255) + ), + start = Offset(0f, 50f), + end = Offset(100f, 50f) + ) + } + 0x49, 0x4C -> { + Brush.linearGradient( + colors = listOf( + Color(222,216,185), + Color(255,255,255) + ), + start = Offset(0f, 50f), + end = Offset(100f, 50f) + ) + } + else -> { + Brush.linearGradient( + colors = listOf( + Color(222,216,185), + Color(255,255,255) + ), + //start = Offset(0f, 50f), + //end = Offset(100f, 50f) + ) + } + } + + val dropShadowColor = when (laserStatus) { + 0x52 -> { + Color(30,64,175) + } + 0x49, 0x4C -> { + Color(123,49,49).copy(alpha = 0.8f) + } + else -> { + Color(0,0,0) + } + } + + val buttonShape = RoundedCornerShape( + topStart = 60.px.dp, + topEnd = 20.px.dp, + bottomStart = 20.px.dp, + bottomEnd = 60.px.dp + ) + + Row( + modifier = modifier + .noRippleClickable(onClick = { onClick.invoke() }) + .size(280.px.dp, 80.px.dp) + .clip( + buttonShape + ) + .background( + brush = backgroundBrush + ) + .border( + width = 0.5.px.dp, + brush = borderBrush, + shape = buttonShape + ).dropShadow( + shape = RoundedCornerShape( + topStart = 60.px.dp, + topEnd = 20.px.dp, + bottomStart = 20.px.dp, + bottomEnd = 60.px.dp + ), + color = dropShadowColor, + blur = 0.px.dp, + offsetX = 0.px.dp, + offsetY = 1.66.px.dp, + spread = 0.px.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = iconResource), + contentDescription = null, + modifier = Modifier + .size(28.px.dp, 32.px.dp), + contentScale = ContentScale.Crop, + ) + + Spacer(modifier = Modifier.width(5.px.dp)) + + Text( + text = buttonText, + style = RobotoTypography.bodySmall.copy( + brush = textBrush, + fontWeight = FontWeight.ExtraLight, + letterSpacing = (0).sp, + ), + fontSize = 36.px.sp + ) + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun StandByButtonPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + StandByButton( + //laserStatus = 0x52, // ready + laserStatus = 0x49, // laser on + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/home/standby/StandByPopup.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/home/standby/StandByPopup.kt new file mode 100644 index 0000000..f83139c --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/home/standby/StandByPopup.kt @@ -0,0 +1,46 @@ +package com.laseroptek.raman.ui.screens.home.standby + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.utils.ext.noRippleClickable +import timber.log.Timber + +@Composable +fun StandByPopup( + onClick: () -> Unit = {} +) { + FullScreen( + backgroundColor = Color.Transparent + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .noRippleClickable { + Timber.d("StandByPopup background clicked") + onClick() + } + ) + } +} + + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun StandByPopupPreview( +) { + StandByPopup() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/info/InfoScreen.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/info/InfoScreen.kt new file mode 100644 index 0000000..3cebc50 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/info/InfoScreen.kt @@ -0,0 +1,1024 @@ +package com.laseroptek.raman.ui.screens.info + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.common.button.CheckBoxWithText +import com.laseroptek.raman.ui.screens.info.base.LineChartGraph +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber +import java.util.Locale + +@Composable +fun InfoScreen( + paddingValues: PaddingValues = PaddingValues(), + mainViewModel: MainViewModel = hiltViewModel(), + infoViewModel: InfoViewModel = hiltViewModel(), +) { + var showSoftwareInformationDialog by remember{ mutableStateOf(false) } + + val chartDataQueue by mainViewModel.chartDataQueue.collectAsState() + + /* + var intTempState by remember { mutableStateOf(true) } + var extTempState by remember { mutableStateOf(true) } + var intHumidityState by remember { mutableStateOf(true) } + var extHumidityState by remember { mutableStateOf(true) } + + var ktpState by remember { mutableStateOf(true) } + var chamber1State by remember { mutableStateOf(true) } + var chamber2State by remember { mutableStateOf(true) } + var basePlateState by remember { mutableStateOf(true) } + var waterState by remember { mutableStateOf(true) } + */ + + val chartUiState by infoViewModel.chartUiState.collectAsState() + + val version by mainViewModel.version.collectAsState() + + val currentTemperature by mainViewModel.temperature.collectAsState() + val lampCount by mainViewModel.lampCount.collectAsState() + + var isSystemMonitoring by remember { mutableStateOf(true) } + val selectedBorderBrush = Brush.verticalGradient( + colors = listOf( + Color(92, 99, 107), + Color(34, 37, 40), + ) + ) + val selectedBackgroundBrush = Brush.verticalGradient( + colors = listOf( + Color(47,51,57), + Color(83, 88, 97), + ) + ) + val selecteDropShadowColor1 = Color(243,205,31).copy(alpha = 0.2f) + val selecteDropShadowColor2 = Color(0,0,0).copy(alpha = 0.1f) + + Timber.d("currentTemperature: ${currentTemperature}") + Timber.d("lampCount: ${lampCount}") + + Box { + // Main. + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding( + start = 130.px.dp, + end = 30.px.dp, + top = 30.px.dp, + bottom = 30.px.dp + ), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // Info_Top_Layer + Box( + modifier = Modifier + .fillMaxWidth() + .height(110.px.dp) + .background(Color.Transparent) + ) { + // Flash Lamp Total Count (Text) & Shot Count + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .size(width = 250.px.dp, height = 80.px.dp) + ) { + // Flash Lamp Total Count (Text) + Text( + text = stringResource(R.string.flash_lamp_tot_cnt), + style = RobotoTypography.bodyMedium.copy( + textAlign = TextAlign.End, + fontSize = 20.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.px.sp, + ), + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.weight(1f)) + + // Shot Count + Row ( + modifier = Modifier + .background( + Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(250, 250, 250), + ) + ), + shape = RoundedCornerShape(12.px.dp) + ) + .border( + width = 1.px.dp, + shape = RoundedCornerShape(12.px.dp), + color = Color(209, 209, 209) + ) + .size(width = 260.px.dp, height = 52.px.dp), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + text = String.format(Locale.US, "%9d", lampCount), + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 32.px.sp, + ), + textAlign = TextAlign.Center, + color = Color.Black + ) + Spacer(Modifier.width(10.px.dp)) + Text( + text = stringResource(R.string.shots), + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Center, + color = Color.Black + ) + Spacer(Modifier.width(20.px.dp)) + } + } + + // Software Info - Button + TextButton( + modifier = Modifier + .align(Alignment.TopEnd) + .size(width = 150.px.dp, height = 44.px.dp) + .clip(RoundedCornerShape(100.px.dp)) + .background(Color(35, 35, 33)), + onClick = { + showSoftwareInformationDialog = true + } + ) { + Text( + text = stringResource(R.string.software_info), + style = RobotoTypography.bodyMedium.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.px.sp, + ), + textAlign = TextAlign.Center, + color = Color(243,205,81) + ) + } + } + + Spacer(Modifier.height(20.px.dp)) + + // Tab + Box( + modifier = Modifier + .fillMaxWidth() + .height(50.px.dp) + .background(Color.Transparent) + ) { + Row( + modifier = + Modifier + .size(406.px.dp, 46.px.dp) + .border( + width = 1.px.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(10.px.dp) + ), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // Environment Monitoring - Title + Row( + modifier = Modifier + .noRippleClickable { + if (isSystemMonitoring == true) { + isSystemMonitoring = false + + /* + intTempState = true + extTempState = true + intHumidityState = true + extHumidityState = true + + ktpState = false + chamber1State = false + chamber2State = false + basePlateState = false + waterState = false + */ + } + } + .size(200.px.dp, 40.px.dp) + .clip(RoundedCornerShape(8.px.dp)) + .background( + brush = + if (isSystemMonitoring == false) selectedBackgroundBrush + else SolidColor(Color.Transparent), + ) + .border( + width = 1.5.px.dp, + brush = if (isSystemMonitoring == false) selectedBorderBrush else SolidColor( + Color.Transparent + ), + shape = RoundedCornerShape(8.px.dp) + ) + .background( + brush = + if (isSystemMonitoring == false) selectedBackgroundBrush + else SolidColor(Color.Transparent), + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (isSystemMonitoring == false) selecteDropShadowColor1 else Color.Transparent, + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (isSystemMonitoring == false) selecteDropShadowColor2 else Color.Transparent, + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.environment_monitoring), + style = RobotoTypography.bodySmall.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.ExtraLight, + ), + textAlign = TextAlign.Center, + color = if (isSystemMonitoring == false) Color.White else Color.Black + ) + } + + Spacer(Modifier.width(1.px.dp)) + + // System Monitoring - Title + Row( + modifier = Modifier + .noRippleClickable { + if (isSystemMonitoring == false) { + isSystemMonitoring = true + + /* + intTempState = false + extTempState = false + intHumidityState = false + extHumidityState = false + + ktpState = true + chamber1State = true + chamber2State = true + basePlateState = true + waterState = true + */ + } + + } + .size(200.px.dp, 40.px.dp) + .clip(RoundedCornerShape(8.px.dp)) + .background( + brush = + if (isSystemMonitoring == true) selectedBackgroundBrush + else SolidColor(Color.Transparent), + ) + .border( + width = 1.5.px.dp, + brush = if (isSystemMonitoring == true) selectedBorderBrush else SolidColor( + Color.Transparent + ), + shape = RoundedCornerShape(8.px.dp) + ) + .background( + brush = + if (isSystemMonitoring == true) selectedBackgroundBrush + else SolidColor(Color.Transparent), + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (isSystemMonitoring == true) selecteDropShadowColor1 else Color.Transparent, + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(8.px.dp), + color = if (isSystemMonitoring == true) selecteDropShadowColor2 else Color.Transparent, + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.system_monitoring), + style = RobotoTypography.bodySmall.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.ExtraLight, + ), + textAlign = TextAlign.Center, + color = if (isSystemMonitoring == true) Color.White else Color.Black + ) + } + } + } + + Spacer(Modifier.height(8.px.dp)) + + if (isSystemMonitoring == false) { + // Environment Monitoring - Frame + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // Current Interal Environment - Frame + Row(modifier = Modifier + .background( + Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(250, 250, 250), + ) + ), + shape = RoundedCornerShape(12.px.dp) + ) + .border( + 1.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(12.px.dp) + ) + .size(514.px.dp, 88.px.dp), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // Current Interal Environment - Text + Column( + modifier = Modifier + .size(155.px.dp, 48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = stringResource(R.string.current_internal_environment), + style = RobotoTypography.bodyMedium.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black, + ) + } + + // TEMP - Text & Value + Column( + modifier = Modifier + .size(155.px.dp, 48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text( + text = "TEMP", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.intTemperature.toInt().toString() + " °C", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + + // Divider + VerticalDivider( + modifier = Modifier.size(2.px.dp, 48.px.dp), + thickness = 2.px.dp, + color = Color.LightGray + ) + + Column( + modifier = Modifier + .size(155.px.dp, 48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "HUMIDITY", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.intHumidity.toInt().toString() + " %", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + // Current External Environment - Frame + Row(modifier = Modifier + .background( + Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(250, 250, 250), + ) + ), + shape = RoundedCornerShape(12.px.dp) + ) + .border( + 1.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(12.px.dp) + ) + .size(514.px.dp, 88.px.dp), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // Current External Environment - Text + Column( + modifier = Modifier + .size(155.px.dp, 48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = stringResource(R.string.current_external_environment), + style = RobotoTypography.bodyMedium.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black, + ) + } + + // TEMP - Text & Value + Column( + modifier = Modifier + .size(155.px.dp, 48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text( + text = "TEMP", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.extTemperature.toInt().toString() + " °C", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + + // Divider + VerticalDivider( + modifier = Modifier.size(2.px.dp, 48.px.dp), + thickness = 2.px.dp, + color = Color.LightGray + ) + + Column( + modifier = Modifier + .size(155.px.dp, 48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "HUMIDITY", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.extHumidity.toInt().toString() + " %", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + } + } + } else { + // System Monitoring - Frame + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // Current Interal Environment - Frame + Row(modifier = Modifier + .background( + Brush.verticalGradient( + colors = listOf( + Color(255, 255, 255), + Color(250, 250, 250), + ) + ), + shape = RoundedCornerShape(12.px.dp) + ) + .border( + 1.dp, + color = Color(209, 209, 209), + shape = RoundedCornerShape(12.px.dp) + ) + .fillMaxWidth() + .height(88.px.dp), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // Current Interal Environment - Text + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp) + .padding(start = 20.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = stringResource(R.string.current_system_temperature), + style = RobotoTypography.bodyMedium.copy( + fontSize = 16.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black, + ) + } + + // KTP - Text & Value + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text( + text = "KTP", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.ktp.toInt().toString() + " °C", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + + // Divider + VerticalDivider( + modifier = Modifier.size(2.px.dp, 48.px.dp), + thickness = 2.px.dp, + color = Color.LightGray + ) + + // Chamber1 - Text & Value + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "CHAMBER1", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.chamber1.toInt().toString() + " °C", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + + // Divider + VerticalDivider( + modifier = Modifier.size(2.px.dp, 48.px.dp), + thickness = 2.px.dp, + color = Color.LightGray + ) + + // Chamber2 - Text & Value + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "CHAMBER2", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.chamber2.toInt().toString() + " °C", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + + // Divider + VerticalDivider( + modifier = Modifier.size(2.px.dp, 48.px.dp), + thickness = 2.px.dp, + color = Color.LightGray + ) + + // Baseplate - Text & Value + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "BASEPLATE", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.basePlate.toInt().toString() + " °C", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + + // Divider + VerticalDivider( + modifier = Modifier.size(2.px.dp, 48.px.dp), + thickness = 2.px.dp, + color = Color.LightGray + ) + + // Water - Text & Value + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.px.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "WATER", + style = RobotoTypography.bodyMedium.copy( + fontSize = 14.px.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.px.sp, + ), + textAlign = TextAlign.Start, + color = Color.Black.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = currentTemperature.water.toInt().toString() + " °C", + style = RobotoTypography.bodyMedium.copy( + fontSize = 26.px.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.px.sp, + ), + color = Color.Black, + ) + } + } + } + } + + + Spacer(Modifier.height(8.px.dp)) + + // Info_Chart_Layer + Row( + modifier = Modifier + .fillMaxWidth() + .height(260.px.dp) + .clip(RoundedCornerShape(size = 12.px.dp)) + .background(color = Color.White) + .border( + 1.px.dp, + color = Color.LightGray, + shape = RoundedCornerShape(size = 12.px.dp) + ), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + LineChartGraph( + isSystemMonitoring = isSystemMonitoring, + chartDataQueue = chartDataQueue, + chartUiState = chartUiState + ) + } + + Spacer(Modifier.height(8.px.dp)) + + // CheckBox List + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.px.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + if (isSystemMonitoring == false) { + // Internal Temp - CheckBox + CheckBoxWithText(label = "Internal Temp", state = chartUiState.intTempState, + onStateChange = { + Timber.d("onStateChange Internal Temp") + //intTempState = it + infoViewModel.toggleLine(ChartLine.INT_TEMP) + } + ) + + // External Temp - CheckBox + CheckBoxWithText(label = "External Temp", state = chartUiState.extTempState, + onStateChange = { + Timber.d("onStateChange External Temp") + //extTempState = it + infoViewModel.toggleLine(ChartLine.EXT_TEMP) + } + ) + + // Internal Humidity - CheckBox + CheckBoxWithText(label = "Internal Humidity", state = chartUiState.intHumidityState, + onStateChange = { + Timber.d("onStateChange Internal Humidity") + //intHumidityState = it + infoViewModel.toggleLine(ChartLine.INT_HUMIDITY) + } + ) + + // External Humidity - CheckBox + CheckBoxWithText(label = "External Humidity", state = chartUiState.extHumidityState, + onStateChange = { + Timber.d("onStateChange External Humidity") + //extHumidityState = it + infoViewModel.toggleLine(ChartLine.EXT_HUMIDITY) + } + ) + } else { + // KTP - CheckBox + CheckBoxWithText(label = "KTP", state = chartUiState.ktpState, + onStateChange = { + Timber.d("onStateChange KTP") + //ktpState = it + infoViewModel.toggleLine(ChartLine.KTP) + } + ) + + // Chamber1 - CheckBox + CheckBoxWithText(label = "Chamber1", state = chartUiState.chamber1State, + onStateChange = { + Timber.d("onStateChange Chamber1") + //chamber1State = it + infoViewModel.toggleLine(ChartLine.CHAMBER1) + } + ) + + // Chamber2 - CheckBox + CheckBoxWithText(label = "Chamber2", state = chartUiState.chamber2State, + onStateChange = { + Timber.d("onStateChange Chamber2") + //chamber2State = it + infoViewModel.toggleLine(ChartLine.CHAMBER2) + } + ) + + // BasePlate - CheckBox + CheckBoxWithText(label = "BasePlate", state = chartUiState.basePlateState, + onStateChange = { + Timber.d("onStateChange BasePlate") + //basePlateState = it + infoViewModel.toggleLine(ChartLine.BASE_PLATE) + } + ) + + // Water - CheckBox + CheckBoxWithText(label = "Water", state = chartUiState.waterState, + onStateChange = { + Timber.d("onStateChange Water") + //waterState = it + infoViewModel.toggleLine(ChartLine.WATER) + } + ) + } + } + + Spacer(Modifier.weight(1f)) + } + + /////////////////////////////////////////// + // Popups + + if (showSoftwareInformationDialog) { + SoftwareInformationDialog( + //showSoftwareInformationDialog = mainViewModel.showSoftwareInformationDialog, + version = version, + title = stringResource(R.string.software_information_title), + onClick = { + Timber.d("SoftwareInformationDialog onClick") + showSoftwareInformationDialog = false + } + ) + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewInfoScreen( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + InfoScreen( + mainViewModel = mainViewModel, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/info/InfoViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/info/InfoViewModel.kt new file mode 100644 index 0000000..fc035de --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/info/InfoViewModel.kt @@ -0,0 +1,59 @@ +package com.laseroptek.raman.ui.screens.info + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber +import javax.inject.Inject + +// An enum to clearly identify each line, making the code type-safe. +enum class ChartLine { + INT_TEMP, EXT_TEMP, INT_HUMIDITY, EXT_HUMIDITY, + KTP, CHAMBER1, CHAMBER2, BASE_PLATE, WATER +} + +// A single data class to hold the state of all checkboxes. +data class ChartUiState( + val intTempState: Boolean = true, + val extTempState: Boolean = true, + val intHumidityState: Boolean = true, + val extHumidityState: Boolean = true, + val ktpState: Boolean = true, + val chamber1State: Boolean = true, + val chamber2State: Boolean = true, + val basePlateState: Boolean = true, + val waterState: Boolean = true, +) + +@HiltViewModel +class InfoViewModel @Inject constructor( +) : ViewModel() { + + // This is the single source of truth for the checkbox states. + private val _chartUiState = MutableStateFlow(ChartUiState()) + val chartUiState = _chartUiState.asStateFlow() + + // A single, clean function to handle toggling any checkbox. + fun toggleLine(line: ChartLine) { + // .update is a thread-safe way to update the state. + _chartUiState.update { currentState -> + when (line) { + ChartLine.INT_TEMP -> currentState.copy(intTempState = !currentState.intTempState) + ChartLine.EXT_TEMP -> currentState.copy(extTempState = !currentState.extTempState) + ChartLine.INT_HUMIDITY -> currentState.copy(intHumidityState = !currentState.intHumidityState) + ChartLine.EXT_HUMIDITY -> currentState.copy(extHumidityState = !currentState.extHumidityState) + ChartLine.KTP -> currentState.copy(ktpState = !currentState.ktpState) + ChartLine.CHAMBER1 -> currentState.copy(chamber1State = !currentState.chamber1State) + ChartLine.CHAMBER2 -> currentState.copy(chamber2State = !currentState.chamber2State) + ChartLine.BASE_PLATE -> currentState.copy(basePlateState = !currentState.basePlateState) + ChartLine.WATER -> currentState.copy(waterState = !currentState.waterState) + } + } + } + + init { + Timber.d("InfoViewModel init") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/info/SoftwareInformationDialog.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/info/SoftwareInformationDialog.kt new file mode 100644 index 0000000..d080324 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/info/SoftwareInformationDialog.kt @@ -0,0 +1,246 @@ +package com.laseroptek.raman.ui.screens.info + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.data.model.serial.Version +import com.laseroptek.raman.ui.common.fullscreen.FullScreenPopup +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.formatWithDots +import com.laseroptek.raman.utils.ext.px + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SoftwareInformationDialog( + title: String = stringResource(R.string.software_information_title), + version: Version = Version(), + onClick: () -> Unit= {} +) { + FullScreenPopup { + Column( + modifier = Modifier + .size(330.px.dp, 304.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color.Black.copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Software version + Row( + modifier = Modifier + .fillMaxWidth() + .height(72.px.dp) + .padding(start = 20.px.dp, end = 20.px.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = title, + style = RobotoTypography.bodySmall.copy( + fontWeight = FontWeight.Thin, + ), + fontWeight = FontWeight.Thin, + fontSize = 22.px.sp, + color = Color.Black, + textAlign = TextAlign.Center, + ) + } + + Column( + modifier = Modifier + .size(282.px.dp, 144.px.dp) + .clip(RoundedCornerShape(12.px.dp)) + .background(Color(250, 250, 250)) + , + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + // Product + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.px.dp) + .padding(start = 15.px.dp, end = 15.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Product", + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.weight(1f)) + Text( + text = "Code: %s, Name: %s".format(version.verCode.formatWithDots(), version.verName), + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + ) + } + + HorizontalDivider( + modifier = Modifier.padding(start = 16.px.dp, end=16.px.dp), + thickness = 0.1.px.dp, + color = Color.Gray + ) + + // Laser hand + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.px.dp) + .padding(start = 15.px.dp, end = 15.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Laser Hand", + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.weight(1f)) + Text( + text = "H/W: %s, S/W: %s".format(version.lhHwRev.formatWithDots(), version.lhHwRev.formatWithDots()), + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + ) + } + + HorizontalDivider( + modifier = Modifier.padding(start = 16.px.dp, end=16.px.dp), + thickness = 0.1.px.dp, + color = Color.Gray + ) + + // Power Supply + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.px.dp) + .padding(start = 15.px.dp, end = 15.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Power Supply", + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 14.px.sp, + color = Color.Black.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.weight(1f)) + Text( + text = "H/W: %s, S/W: %s".format(version.psHwVer.formatWithDots(), version.psSwVer.formatWithDots()), + fontWeight = FontWeight.Thin, + style = RobotoTypography.bodySmall.copy( + letterSpacing = 0.25.px.sp + ), + fontSize = 12.px.sp, + color = Color.Black.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + ) + } + } + + // close button + Row( + modifier = Modifier + .fillMaxWidth() + .height(88.px.dp) + .padding(start = 24.px.dp, end = 12.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + modifier = Modifier + .size(92.px.dp, 88.px.dp), + onClick = onClick + ) { + Text( + text = stringResource(R.string.close), + style = RobotoTypography.labelLarge, + fontWeight = FontWeight.Normal, + fontSize = 14.px.sp, + color = Color.Black, + textAlign = TextAlign.End + ) + } + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewSoftwareInformationDialog() { + SoftwareInformationDialog() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LabelHelper.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LabelHelper.kt new file mode 100644 index 0000000..ae595df --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LabelHelper.kt @@ -0,0 +1,69 @@ +package com.laseroptek.raman.ui.screens.info.base + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.data.model.chart.Bars + +@Composable +fun LabelHelper( + data: List>, + textStyle: TextStyle = TextStyle.Default.copy(fontSize = 13.sp) +) { + LazyVerticalGrid(columns = GridCells.Fixed(data.size), modifier = Modifier) { + items(data) { (label, color) -> + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(color) + ) + BasicText(text = label, style = textStyle) + } + } + } +} + +/** + * RC means Row & Column + */ +@Composable +fun RCChartLabelHelper( + data:List, + textStyle: TextStyle = TextStyle.Default.copy(fontSize = 13.sp) +) { + val labels = data.flatMap { it.values.map { it.label } }.distinct() + val colors = labels.map { label -> + data.flatMap { bars -> + bars.values.filter { it.label == label }.map { it.color } + }.firstOrNull() ?: SolidColor(Color.Unspecified) + } + LabelHelper( + data = labels.mapIndexed { index, label -> label.orEmpty() to colors[index] }, + textStyle = textStyle + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LineChart.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LineChart.kt new file mode 100644 index 0000000..9486a00 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LineChart.kt @@ -0,0 +1,654 @@ +package com.laseroptek.raman.ui.screens.info.base + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.PathMeasure +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.toSize +import com.laseroptek.raman.data.model.chart.AnimationMode +import com.laseroptek.raman.data.model.chart.DividerProperties +import com.laseroptek.raman.data.model.chart.DotProperties +import com.laseroptek.raman.data.model.chart.DrawStyle +import com.laseroptek.raman.data.model.chart.GridProperties +import com.laseroptek.raman.data.model.chart.HorizontalIndicatorProperties +import com.laseroptek.raman.data.model.chart.IndicatorPosition +import com.laseroptek.raman.data.model.chart.LabelHelperProperties +import com.laseroptek.raman.data.model.chart.LabelProperties +import com.laseroptek.raman.data.model.chart.Line +import com.laseroptek.raman.data.model.chart.PopupProperties +import com.laseroptek.raman.data.model.chart.ZeroLineProperties +import com.laseroptek.raman.utils.calculateOffset +import com.laseroptek.raman.utils.ext.drawGridLines +import com.laseroptek.raman.utils.ext.spaceBetween +import com.laseroptek.raman.utils.split +import ir.ehsannarmani.compose_charts.extensions.line_chart.drawLineGradient +import ir.ehsannarmani.compose_charts.extensions.line_chart.getLinePath +import ir.ehsannarmani.compose_charts.extensions.line_chart.getPopupValue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private data class Popup( + val properties: PopupProperties, + val position: Offset, + val value: Double +) + +@Composable +fun LineChart( + modifier: Modifier = Modifier, + data: List, + curvedEdges: Boolean = true, + animationDelay: Long = 300, + animationMode: AnimationMode = AnimationMode.Together(), + dividerProperties: DividerProperties = DividerProperties(), + gridProperties: GridProperties = GridProperties(), + zeroLineProperties: ZeroLineProperties = ZeroLineProperties(), + indicatorProperties: HorizontalIndicatorProperties = HorizontalIndicatorProperties( + textStyle = TextStyle.Default, + padding = 16.dp + ), + labelHelperProperties: LabelHelperProperties = LabelHelperProperties(), + labelHelperPadding: Dp = 26.dp, + textMeasurer: TextMeasurer = rememberTextMeasurer(), + popupProperties: PopupProperties = PopupProperties( + textStyle = TextStyle.Default.copy( + color = Color.White, + fontSize = 12.sp + ) + ), + dotsProperties: DotProperties = DotProperties(), + labelProperties: LabelProperties = LabelProperties(enabled = false), + maxValue: Double = data.maxOfOrNull { it.values.maxOfOrNull { it } ?: 0.0 } ?: 0.0, + minValue: Double = if (data.any { it.values.any { it < 0.0 } }) data.minOfOrNull { + it.values.minOfOrNull { it } ?: 0.0 + } ?: 0.0 else 0.0, +) { + if (data.isNotEmpty()) { + require(minValue <= (data.minOfOrNull { it.values.minOfOrNull { it } ?: 0.0 } ?: 0.0)) { + "Chart data must be at least $minValue (Specified Min Value)" + } + require(maxValue >= (data.maxOfOrNull { it.values.maxOfOrNull { it } ?: 0.0 } ?: 0.0)) { + "Chart data must be at most $maxValue (Specified Max Value)" + } + } + + val density = LocalDensity.current + val scope = rememberCoroutineScope() + + val pathMeasure = remember { + PathMeasure() + } + + val popupAnimation = remember { + Animatable(0f) + } + + val zeroLineAnimation = remember { + Animatable(0f) + } + + val dotAnimators = remember { + mutableStateListOf>>() + } + val popups = remember { + mutableStateListOf() + } + val popupsOffsetAnimators = remember { + mutableStateListOf, Animatable>>() + } + val labelAreaHeight = remember { + if (labelProperties.enabled) { + if (labelProperties.labels.isNotEmpty()) { + labelProperties.labels.maxOf { + textMeasurer.measure( + it, + style = labelProperties.textStyle + ).size.height + } + (labelProperties.padding.value * density.density).toInt() + } else { + error("Labels enabled, but there is no label provided to show, disable labels or fill 'labels' parameter in LabelProperties") + } + } else { + 0 + } + } + + LaunchedEffect(Unit) { + if (zeroLineProperties.enabled) { + zeroLineAnimation.snapTo(0f) + zeroLineAnimation.animateTo(1f, animationSpec = zeroLineProperties.animationSpec) + } + } + + // make animators + LaunchedEffect(data) { + dotAnimators.clear() + launch { + data.forEach { + val animators = mutableListOf>() + it.values.forEach { + animators.add(Animatable(0f)) + } + dotAnimators.add(animators) + } + } + } + + // animate + LaunchedEffect(data) { + delay(animationDelay) + + val animateStroke: suspend (Line) -> Unit = { line -> + line.strokeProgress.animateTo(1f, animationSpec = line.strokeAnimationSpec) + } + val animateGradient: suspend (Line) -> Unit = { line -> + delay(line.gradientAnimationDelay) + line.gradientProgress.animateTo(1f, animationSpec = line.gradientAnimationSpec) + } + launch { + data.forEachIndexed { index, line -> + when (animationMode) { + is AnimationMode.OneByOne -> { + animateStroke(line) + } + + is AnimationMode.Together -> { + launch { + delay(animationMode.delayBuilder(index)) + animateStroke(line) + } + } + } + } + } + launch { + data.forEachIndexed { index, line -> + when (animationMode) { + is AnimationMode.OneByOne -> { + animateGradient(line) + } + + is AnimationMode.Together -> { + launch { + delay(animationMode.delayBuilder(index)) + animateGradient(line) + } + } + } + } + } + } + + Column(modifier = modifier) { + if (labelHelperProperties.enabled) { + LabelHelper( + data = data.map { it.label to it.color }, + textStyle = labelHelperProperties.textStyle + ) + Spacer(modifier = Modifier.height(labelHelperPadding)) + } + Row(modifier = Modifier.fillMaxSize()) { + val paddingBottom = (labelAreaHeight / density.density).dp + if (indicatorProperties.enabled) { + if (indicatorProperties.position == IndicatorPosition.Horizontal.Start) { + Indicators( + modifier = Modifier.padding(bottom = paddingBottom), + indicatorProperties = indicatorProperties, + minValue = minValue, + maxValue = maxValue + ) + Spacer(modifier = Modifier.width(indicatorProperties.padding)) + } + } + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Canvas(modifier = Modifier + .weight(1f) + .fillMaxSize() + .pointerInput(data, minValue, maxValue) { + detectDragGestures( + onDragEnd = { + scope.launch { + popupAnimation.animateTo(0f, animationSpec = tween(500)) + popups.clear() + popupsOffsetAnimators.clear() + } + }, + onDrag = { change, amount -> + val _size = size.toSize() + .copy(height = (size.height - labelAreaHeight).toFloat()) + popups.clear() + data.forEach { + val properties = it.popupProperties ?: popupProperties + + if (properties.enabled) { + val positionX = + (change.position.x).coerceIn( + 0f, + size.width.toFloat() + ) + val fraction = (positionX / size.width) + val popupValue = getPopupValue( + points = it.values, + fraction = fraction.toDouble(), + rounded = it.curvedEdges ?: curvedEdges, + size = _size, + minValue = minValue, + maxValue = maxValue + ) + popups.add( + Popup( + position = popupValue.offset, + value = popupValue.calculatedValue, + properties = properties + ) + ) + // add popup offset animators + if (popupsOffsetAnimators.count() < popups.count()) { + repeat(popups.count() - popupsOffsetAnimators.count()) { + popupsOffsetAnimators.add( + Animatable(0f) to Animatable( + 0f + ) + ) + } + } + } + } + scope.launch { + // animate popup (alpha) + if (popupAnimation.value != 1f && !popupAnimation.isRunning) { + popupAnimation.animateTo(1f, animationSpec = tween(500)) + } + } + } + ) + } + ) { + val chartAreaHeight = size.height - labelAreaHeight + val drawZeroLine = { + val zeroY = chartAreaHeight - calculateOffset( + minValue = minValue, + maxValue = maxValue, + total = chartAreaHeight, + value = 0f + ).toFloat() + drawLine( + brush = zeroLineProperties.color, + start = Offset(x = 0f, y = zeroY), + end = Offset(x = size.width * zeroLineAnimation.value, y = zeroY), + pathEffect = zeroLineProperties.style.pathEffect, + strokeWidth = zeroLineProperties.thickness.toPx() + ) + } + + if (labelProperties.enabled) { + labelProperties.labels.forEachIndexed { index, label -> + val measureResult = + textMeasurer.measure(label, style = labelProperties.textStyle) + drawText( + textLayoutResult = measureResult, + topLeft = Offset( + (size.width - measureResult.size.width).spaceBetween( + itemCount = labelProperties.labels.count(), + index = index + ), + size.height - labelAreaHeight + labelProperties.padding.toPx() + ) + ) + } + } + + drawGridLines( + dividersProperties = dividerProperties, + indicatorPosition = indicatorProperties.position, + xAxisProperties = gridProperties.xAxisProperties, + yAxisProperties = gridProperties.yAxisProperties, + size = size.copy(height = chartAreaHeight), + gridEnabled = gridProperties.enabled + ) + + if (zeroLineProperties.enabled && zeroLineProperties.zType == ZeroLineProperties.ZType.Under) { + drawZeroLine() + } + + data.forEachIndexed { index, line -> + val path = getLinePath( + dataPoints = line.values.map { it.toFloat() }, + maxValue = maxValue.toFloat(), + minValue = minValue.toFloat(), + rounded = line.curvedEdges ?: curvedEdges, + size = size.copy(height = chartAreaHeight) + ) + val segmentedPath = Path() + pathMeasure.setPath(path, false) + pathMeasure.getSegment( + 0f, + pathMeasure.length * line.strokeProgress.value, + segmentedPath + ) + var pathEffect: PathEffect? = null + val stroke: Float = when (val drawStyle = line.drawStyle) { + is DrawStyle.Fill -> { + 0f + } + + is DrawStyle.Stroke -> { + pathEffect = drawStyle.strokeStyle.pathEffect + drawStyle.width.toPx() + } + } + + // + // draw path (line) between data points + // + drawPath( + path = segmentedPath, + brush = line.color, + style = Stroke(width = stroke, pathEffect = pathEffect) + ) + + if (line.firstGradientFillColor != null && line.secondGradientFillColor != null) { + drawLineGradient( + path = path, + color1 = line.firstGradientFillColor, + color2 = line.secondGradientFillColor, + progress = line.gradientProgress.value, + size = size.copy(height = chartAreaHeight) + ) + } else if (line.drawStyle is DrawStyle.Fill) { + var fillColor = Color.Unspecified + if (line.color is SolidColor) { + fillColor = line.color.value + } + drawLineGradient( + path = path, + color1 = fillColor, + color2 = fillColor, + progress = 1f, + size = size.copy(height = chartAreaHeight) + ) + } + + // data point + if ((line.dotProperties?.enabled ?: dotsProperties.enabled)) { + drawDots( + dataPoints = line.values.mapIndexed { mapIndex, value -> + (dotAnimators.getOrNull( + index + )?.getOrNull(mapIndex) ?: Animatable(0f)) to value.toFloat() + }, + properties = line.dotProperties ?: dotsProperties, + linePath = segmentedPath, + maxValue = maxValue.toFloat(), + minValue = minValue.toFloat(), + pathMeasure = pathMeasure, + scope = scope, + size = size.copy(height = chartAreaHeight) + ) + } + } + if (zeroLineProperties.enabled && zeroLineProperties.zType == ZeroLineProperties.ZType.Above) { + drawZeroLine() + } + popups.forEachIndexed { index, popup -> + drawPopup( + popup = popup, + nextPopup = popups.getOrNull(index + 1), + textMeasurer = textMeasurer, + scope = scope, + progress = popupAnimation.value, + offsetAnimator = popupsOffsetAnimators.getOrNull(index) + ) + } + } + } + if (indicatorProperties.enabled) { + if (indicatorProperties.position == IndicatorPosition.Horizontal.End) { + Spacer(modifier = Modifier.width(indicatorProperties.padding)) + Indicators( + modifier = Modifier.padding(bottom = paddingBottom), + indicatorProperties = indicatorProperties, + minValue = minValue, + maxValue = maxValue + ) + } + } + } + } +} + +@Composable +private fun Indicators( + modifier: Modifier = Modifier, + indicatorProperties: HorizontalIndicatorProperties, + minValue: Double, + maxValue: Double +) { + Column( + modifier = modifier + .fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween + ) { + split( + count = indicatorProperties.count, + minValue = minValue, + maxValue = maxValue + ).forEach { + BasicText( + text = indicatorProperties.contentBuilder(it), + style = indicatorProperties.textStyle + ) + } + } +} + +private fun DrawScope.drawPopup( + popup: Popup, + nextPopup: Popup?, + textMeasurer: TextMeasurer, + scope: CoroutineScope, + progress: Float, + offsetAnimator: Pair, Animatable>? = null, +) { + val offset = popup.position + val popupProperties = popup.properties + val measureResult = textMeasurer.measure( + popupProperties.contentBuilder(popup.value), + style = popupProperties.textStyle.copy( + color = popupProperties.textStyle.color.copy( + alpha = 1f * progress + ) + ) + ) + var rectSize = measureResult.size.toSize() + rectSize = rectSize.copy( + width = (rectSize.width + (popupProperties.contentHorizontalPadding.toPx() * 2)), + height = (rectSize.height + (popupProperties.contentVerticalPadding.toPx() * 2)) + ) + + val conflictDetected = + ((nextPopup != null) && offset.y in nextPopup.position.y - rectSize.height..nextPopup.position.y + rectSize.height) || + (offset.x + rectSize.width) > size.width + + + val rectOffset = if (conflictDetected) { + offset.copy(x = offset.x - rectSize.width) + } else { + offset + } + offsetAnimator?.also { (x, y) -> + if (x.value == 0f || y.value == 0f) { + scope.launch { + x.snapTo(rectOffset.x) + y.snapTo(rectOffset.y) + } + } else { + scope.launch { + x.animateTo(rectOffset.x) + } + scope.launch { + y.animateTo(rectOffset.y) + } + } + + } + if (offsetAnimator != null) { + val animatedOffset = Offset( + x = offsetAnimator.first.value, + y = offsetAnimator.second.value + ) + val rect = Rect( + offset = animatedOffset, + size = rectSize + ) + drawPath( + path = Path().apply { + addRoundRect( + RoundRect( + rect = rect.copy( + top = rect.top, + left = rect.left, + ), + topLeft = CornerRadius( + if (conflictDetected) popupProperties.cornerRadius.toPx() else 0f, + if (conflictDetected) popupProperties.cornerRadius.toPx() else 0f + ), + topRight = CornerRadius( + if (!conflictDetected) popupProperties.cornerRadius.toPx() else 0f, + if (!conflictDetected) popupProperties.cornerRadius.toPx() else 0f + ), + bottomRight = CornerRadius( + popupProperties.cornerRadius.toPx(), + popupProperties.cornerRadius.toPx() + ), + bottomLeft = CornerRadius( + popupProperties.cornerRadius.toPx(), + popupProperties.cornerRadius.toPx() + ), + ) + ) + }, + color = popupProperties.containerColor, + alpha = 1f * progress + ) + drawText( + textLayoutResult = measureResult, + topLeft = animatedOffset.copy( + x = animatedOffset.x + popupProperties.contentHorizontalPadding.toPx(), + y = animatedOffset.y + popupProperties.contentVerticalPadding.toPx() + ) + ) + } +} + +fun DrawScope.drawDots( + dataPoints: List, Float>>, + properties: DotProperties, + linePath: Path, + maxValue: Float, + minValue: Float, + pathMeasure: PathMeasure, + scope: CoroutineScope, + size: Size? = null, +) { + val _size = size ?: this.size + + val pathEffect = properties.strokeStyle.pathEffect + + pathMeasure.setPath(linePath, false) + val lastPosition = pathMeasure.getPosition(pathMeasure.length) + dataPoints.forEachIndexed { valueIndex, value -> + val dotOffset = Offset( + x = _size.width.spaceBetween( + itemCount = dataPoints.count(), + index = valueIndex + ), + y = (_size.height - calculateOffset( + maxValue = maxValue.toDouble(), + minValue = minValue.toDouble(), + total = _size.height, + value = value.second + )).toFloat() + + ) + if (lastPosition != Offset.Unspecified && lastPosition.x >= dotOffset.x - 20 || !properties.animationEnabled) { + if (!value.first.isRunning && properties.animationEnabled && value.first.value != 1f) { + scope.launch { + value.first.animateTo(1f, animationSpec = properties.animationSpec) + } + } + + val radius: Float + val strokeRadius: Float + if (properties.animationEnabled) { + radius = + (properties.radius.toPx() + properties.strokeWidth.toPx() / 2) * value.first.value + strokeRadius = properties.radius.toPx() * value.first.value + } else { + radius = properties.radius.toPx() + properties.strokeWidth.toPx() / 2 + strokeRadius = properties.radius.toPx() + } + drawCircle( + brush = properties.strokeColor, + radius = radius, + center = dotOffset, + style = Stroke(width = properties.strokeWidth.toPx(), pathEffect = pathEffect), + ) + drawCircle( + brush = properties.color, + radius = strokeRadius, + center = dotOffset, + ) + } + } +} + + + + + + + + diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LineChartGraph.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LineChartGraph.kt new file mode 100644 index 0000000..c19938b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/info/base/LineChartGraph.kt @@ -0,0 +1,267 @@ +package com.laseroptek.raman.ui.screens.info.base + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.MAX_CHART_COLUMN_DATA_SIZE +import com.laseroptek.raman.data.model.chart.AnimationMode +import com.laseroptek.raman.data.model.chart.DividerProperties +import com.laseroptek.raman.data.model.chart.DotProperties +import com.laseroptek.raman.data.model.chart.GridProperties +import com.laseroptek.raman.data.model.chart.HorizontalIndicatorProperties +import com.laseroptek.raman.data.model.chart.LabelHelperProperties +import com.laseroptek.raman.data.model.chart.LabelProperties +import com.laseroptek.raman.data.model.chart.Line +import com.laseroptek.raman.data.model.chart.LineProperties +import com.laseroptek.raman.data.model.chart.PopupProperties +import com.laseroptek.raman.data.model.chart.StrokeStyle +import com.laseroptek.raman.data.model.serial.TemperatureTimeStamp +import com.laseroptek.raman.ui.screens.info.ChartUiState +import com.laseroptek.raman.utils.FixedSizeLIFOQueue +import com.laseroptek.raman.utils.ext.convertListToMutableStateList +import com.laseroptek.raman.utils.ext.convertListToMutableStateListBasedOnBoolean +import com.laseroptek.raman.utils.ext.format +import com.laseroptek.raman.utils.ext.px +import com.laseroptek.raman.utils.ext.toHHMMSSString +import com.laseroptek.raman.utils.ext.toYYYYMMDDHHMMSSString +import timber.log.Timber + +val ubuntu @Composable +get() = FontFamily( + Font(R.font.montserrat_regular, FontWeight.Normal), + Font(R.font.montserrat_bold, FontWeight.Bold) +) + +val gridProperties = GridProperties( + xAxisProperties = GridProperties.AxisProperties( + thickness = .2.dp, + color = SolidColor(Color.Gray.copy(alpha = .5f)), + style = StrokeStyle.Dashed(intervals = floatArrayOf(15f,15f), phase = 10f), + ), + yAxisProperties = GridProperties.AxisProperties( + thickness = .2.dp, + color = SolidColor(Color.Gray.copy(alpha = .5f)), + style = StrokeStyle.Dashed(intervals = floatArrayOf(15f,15f), phase = 10f), + ), +) +val dividerProperties = DividerProperties( + xAxisProperties = LineProperties( + thickness = .2.dp, + color = SolidColor(Color.Gray.copy(alpha = .5f)), + style = StrokeStyle.Dashed(intervals = floatArrayOf(15f,15f), phase = 10f), + ), + yAxisProperties = LineProperties( + thickness = .2.dp, + color = SolidColor(Color.Gray.copy(alpha = .5f)), + style = StrokeStyle.Dashed(intervals = floatArrayOf(15f,15f), phase = 10f), + ) +) + + + +@Composable +fun LineChartGraph( + chartDataQueue: FixedSizeLIFOQueue = FixedSizeLIFOQueue(MAX_CHART_COLUMN_DATA_SIZE), + isSystemMonitoring: Boolean = false, + chartUiState: ChartUiState = ChartUiState(), +) { + Timber.d("*** chartDataQueue (isSystemMonitoring: $isSystemMonitoring) ***") + + if (chartDataQueue.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = "Waiting for data...", + fontSize = 16.sp, + color = Color.Gray + ) + } + return + } + + val (processedData, timestampLabels) = remember( +chartDataQueue, + isSystemMonitoring, + chartUiState.intTempState, + chartUiState.extTempState, + chartUiState.intHumidityState, + chartUiState.extHumidityState, + chartUiState.ktpState, + chartUiState.chamber1State, + chartUiState.chamber2State, + chartUiState.basePlateState, + chartUiState.waterState + ) { + // 1. get last Max number of data points + val finalData = chartDataQueue.toList().sortedBy{ it.timestamp }.takeLast(MAX_CHART_COLUMN_DATA_SIZE) + + // 2. Create the final data lists from the averaged data + val intTempList = finalData.map { it.intTemperature.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.intTempState) + val extTempList = finalData.map { it.extTemperature.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.extTempState) + val intHumidityList = finalData.map { it.intHumidity.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.intHumidityState) + val extHumidityList = finalData.map { it.extHumidity.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.extHumidityState) + val ktpList = finalData.map { it.ktp.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.ktpState) + val chamber1List = finalData.map { it.chamber1.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.chamber1State) + val chamber2List = finalData.map { it.chamber2.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.chamber2State) + val basePlateList = finalData.map { it.basePlate.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.basePlateState) + val waterList = finalData.map { it.water.toDouble() }.convertListToMutableStateListBasedOnBoolean(chartUiState.waterState) + + // 3. CREATE THE TIMESTAMP LABELS from the averaged data + val finalTimestamps = finalData.map { it.timestamp.toHHMMSSString() } + .convertListToMutableStateList() + + // DEBUG + Timber.d("*** finalTimestamps: ${finalTimestamps.takeLast(25).toList()} size: ${finalTimestamps.toList().size} ***") + + // DEBUG + finalData.takeLast(25).forEachIndexed { idx, data -> + val timestamp = data.timestamp + val timestamps = data.timestamp.toYYYYMMDDHHMMSSString() + Timber.d("[%02d] %s (%s)".format(idx, timestamps, timestamp.toString())) + } + + // Prepare the Line data for the chart + val data1 = listOf( + Line( + label = "Internal Temp", + values = intTempList.toList(), // Pass a copy if Line expects an immutable list + color = SolidColor(Color(255, 0, 0)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(204, 0, 0)), + ) + ), + Line( + label = "External Temp", + values = extTempList.toList(), + color = SolidColor(Color(255, 255, 0)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(204, 204, 0)), + ) + ), + Line( + label = "Internal Humidity", + values = intHumidityList.toList(), + color = SolidColor(Color(0, 255, 0)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(0, 204, 0)), + ) + ), + Line( + label = "External Humidity", + values = extHumidityList.toList(), + color = SolidColor(Color(0, 255, 255)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(0, 204, 204)), + ) + ) + ) + val data2 = listOf( + // Use listOf for an immutable list, or mutableStateListOf if LineChart modifies it + Line( + label = "KTP", + values = ktpList.toList(), // Pass a copy if Line expects an immutable list + color = SolidColor(Color(255, 0, 0)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(204, 0, 0)), + ) + ), + Line( + label = "Chamber1", + values = chamber1List.toList(), + color = SolidColor(Color(255, 255, 0)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(204, 204, 0)), + ) + ), + Line( + label = "Chamber2", + values = chamber2List.toList(), + color = SolidColor(Color(0, 255, 0)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(0, 204, 0)), + ) + ), + Line( + label = "BasePlate", + values = basePlateList.toList(), + color = SolidColor(Color(0, 255, 255)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(0, 204, 204)), + ) + ), + Line( + label = "Water", + values = waterList.toList(), + color = SolidColor(Color(255, 0, 255)), + dotProperties = DotProperties( + strokeColor = SolidColor(Color(204, 0, 204)), + ) + ), + ) + + // Return the map of (processed data, labels) as a Pair + (if (isSystemMonitoring) data2 else data1) to finalTimestamps + } + + LineChart( + modifier = Modifier + .fillMaxSize() + .padding(24.px.dp), + data = processedData, + animationMode = AnimationMode.Together(delayBuilder = { + it * 500L + }), + gridProperties = gridProperties, + dividerProperties = dividerProperties, + popupProperties = PopupProperties( + enabled = false, + textStyle = TextStyle( + fontSize = 8.sp, + color = Color.Black, + fontFamily = ubuntu + ), + contentBuilder = { + it.format(1) // + " Million" + }, + containerColor = Color.Transparent, //Color(0xff414141) + ), + indicatorProperties = HorizontalIndicatorProperties( + enabled = true, // Enable labels on the y-axis (value / n) + textStyle = TextStyle( + fontSize = 8.sp, + color = Color.Black, + fontFamily = ubuntu + ), + contentBuilder = { + it.format(1) // + " M" + } + ), + labelHelperProperties = LabelHelperProperties( + enabled = true, + textStyle = TextStyle(fontSize = 8.sp, fontFamily = ubuntu, color = Color.Black) + ), + curvedEdges = false, + labelProperties = LabelProperties( + enabled = true, // Enable labels on the x-axis (time steamp) + labels = timestampLabels.toList(), + textStyle = TextStyle( + fontSize = 8.sp, + fontFamily = ubuntu, color = Color.Black + ), + ), + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/lock/LockScreen.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/lock/LockScreen.kt new file mode 100644 index 0000000..9d67197 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/lock/LockScreen.kt @@ -0,0 +1,264 @@ +package com.laseroptek.raman.ui.screens.lock + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.R +import com.laseroptek.raman.navigation.LockNavigation +import com.laseroptek.raman.ui.common.button.ButtonWithHIconAndText +import com.laseroptek.raman.ui.common.fullscreen.FullScreenDialog +import com.laseroptek.raman.ui.theme.PretendardTypography +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.delay +import timber.log.Timber +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LockScreen( + mainNavController: NavHostController = rememberNavController(), + lockViewModel:LockViewModel = hiltViewModel() +) { + val focusManager = LocalFocusManager.current + + var showPopup by remember { mutableStateOf( true ) } + var navigation by rememberSaveable { mutableStateOf(LockNavigation.Enter) } + + // Get Current Time + val currentDateTime = remember { mutableStateOf(LocalDateTime.now()) } + val pinNumber = remember { mutableStateOf("") } + + val dateFormatter = DateTimeFormatter.ofPattern("EEE, MMM dd, yyyy") + val timeFormatter = DateTimeFormatter.ofPattern("hh:mm a") + + val formattedDate = currentDateTime.value.format(dateFormatter) + val formattedTime = currentDateTime.value.format(timeFormatter) + + val modifierFullScreen = Modifier.then( + if (navigation == LockNavigation.Lock) { + Modifier + .fillMaxSize() + .background(color = Color.White) //.padding(top = 93.px.dp) + } else { + Modifier + .size(867.px.dp, 1032.px.dp) + .background(Color.Transparent) + } + ) + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + focusManager.clearFocus(force = true) // Hide the keyboard + } + } + + LaunchedEffect(Unit) { + currentDateTime.value = LocalDateTime.now() + delay(60*1000) // Delay for 1 minute + + } + + //FullScreenPopup(showPopup = showLockPopup) { + if (showPopup) { + FullScreenDialog(backgroundColor = Color(120,120,120)) { + //FullScreen(backgroundColor = Color.Black) { + Box( + modifier = modifierFullScreen, + contentAlignment = Alignment.Center + ) { + when (navigation) { + LockNavigation.Enter -> { + Timber.d("Enter") + PinLock( + title = stringResource(id = R.string.lock_title), + lockState = LockNavigation.Enter, + pinNumber = pinNumber, + onClick = { ok -> + if (ok) { + // on ok check created password and go to confirm + Timber.d("pinNumber: ${pinNumber}") + navigation = LockNavigation.EnterConfirm + } else { + // on click cancel -> exit to main screen + showPopup = false + + // goback + mainNavController.popBackStack() + } + } + ) + } + + LockNavigation.EnterConfirm -> { + Timber.d("EnterConfirm") + PinLock( + title = "Confirm the password for device lock", + lockState = LockNavigation.EnterConfirm, + pinNumber = pinNumber, + onClick = { ok -> + if (ok) { + // go to lock + Timber.d("pinNumber: ${pinNumber}") + navigation = LockNavigation.Lock + } else { + // on click cancel -> goto Enter + navigation = LockNavigation.Enter + } + } + ) + } + + LockNavigation.Lock -> { + Timber.d("Lock") + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) { + // Thu, Feb 29, 2024 + Text( + text = formattedDate, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 16.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + + // hh:mm am + Text( + text = formattedTime, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 34.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.height(120.px.dp)) + + // lock icon + Image( + painter = painterResource(id = R.drawable.ic_lock_screen), + contentDescription = null, + modifier = Modifier.size(46.px.dp) + ) + + // text device is locked. + Text( + text = "Device is locked", + style = PretendardTypography.bodyMedium, + fontWeight = FontWeight.Light, + fontSize = 16.px.sp, + color = Color(243,205,81), + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.height(60.px.dp)) + + // button with icon + ButtonWithHIconAndText( + modifier = Modifier + .size(240.px.dp, 40.px.dp) + .border( + width = 1.px.dp, + shape = RoundedCornerShape(100.px.dp), + color = Color(215, 167, 10), + ), + text = stringResource(R.string.unlock), + style = RobotoTypography.bodySmall, + fontSize = 14.px.sp, + textColor = Color(215, 167, 10), + backgroundColor = Color.Transparent, + roundedCornerShape = RoundedCornerShape(100.px.dp), + leadingIcon = { + Image( + modifier = Modifier.size(18.px.dp), + painter = painterResource(id = R.drawable.ic_unlock), + contentDescription = null, + ) + }, + onClick = { + navigation = LockNavigation.ExitConfirm + } + ) + + Spacer(Modifier.height(100.px.dp)) + } + } + + LockNavigation.ExitConfirm -> { + Timber.d("ExitConfirm") + PinLock( + title = "Confirm the password to exit Lock", + lockState = LockNavigation.ExitConfirm, + pinNumber = pinNumber, + onClick = { ok -> + if (ok) { + Timber.d("pinNumber: ${pinNumber}") + showPopup = false + // goback + mainNavController.popBackStack() + } else { + // on click cancel back to Lock Screen Again + navigation = LockNavigation.Lock + } + } + ) + } + } + } + } + } + +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewLockScreen() { + LockScreen() +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/lock/LockViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/lock/LockViewModel.kt new file mode 100644 index 0000000..32aa265 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/lock/LockViewModel.kt @@ -0,0 +1,14 @@ +package com.laseroptek.raman.ui.screens.lock + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class LockViewModel @Inject constructor( +) : ViewModel() { + init { + Timber.d("LockViewModel init") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/lock/PinLock.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/lock/PinLock.kt new file mode 100644 index 0000000..4695414 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/lock/PinLock.kt @@ -0,0 +1,276 @@ +package com.laseroptek.raman.ui.screens.lock + +import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +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.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.navigation.LockNavigation +import com.laseroptek.raman.ui.common.lock.OTPBox +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.delay +import timber.log.Timber + +@SuppressLint("UnusedContentLambdaTargetStateParameter") +@Composable +fun PinLock( + title: String = "", + lockState: LockNavigation = LockNavigation.Enter, + pinNumber: MutableState = mutableStateOf(""), + onClick: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + var pin by remember { mutableStateOf("") } + var animate by remember { mutableStateOf(false) } + var autoShowKeyboard by remember { mutableStateOf(true) } + + LaunchedEffect(animate) { + if (animate) { + // After a short delay (enough for the animation to start), + // reset the trigger. + delay(100) + animate = false + } + } + + DisposableEffect(Unit) { + onDispose { + Timber.d("MyComposable is being disposed!") + autoShowKeyboard = false + } + } + + Box( + modifier = modifier + .fillMaxSize() + , contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .size(448.px.dp, 580.px.dp) + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Center + ) { + // Pin Lock Box + Column( + modifier = Modifier + .size(448.px.dp, 280.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(color = Color.White) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0, 0, 0).copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Icon + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.px.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + Image(painter = painterResource(id = R.drawable.ic_lock_small), + contentDescription = null, + modifier = Modifier.size(24.px.dp) + ) + } + + // Title + Row(modifier = Modifier + .fillMaxWidth() + .height(50.px.dp) + , verticalAlignment = Alignment.CenterVertically + , horizontalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = RobotoTypography.headlineSmall, + fontSize = 22.px.sp, + letterSpacing = 0.px.sp, + fontWeight = FontWeight.Normal, + color = Color.Black + ) + } + + // PinLock + Box( + modifier = Modifier + .size(398.px.dp, 100.px.dp), + contentAlignment = Alignment.Center + ) { + AnimatedContent( + targetState = animate, + transitionSpec = { + slideInHorizontally( + animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh), + initialOffsetX = { fullWidth -> fullWidth } + ).togetherWith( + slideOutHorizontally( + animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh), + targetOffsetX = { 0 } + ) + ) + }, + ) { + OTPBox( + keyboardType = KeyboardType.NumberPassword, + pinLength = 6, + pinValue = pin, + onPinValueChanged = { newPin -> + Timber.d("onPinValueChanged: pin = ${newPin}") + pin = newPin + }, + onDone = { + Timber.d("onDone") + when (lockState) { + LockNavigation.Enter -> { + if (pin.length == 6) { + autoShowKeyboard = false + pinNumber.value = pin + onClick(true) + } else { + pin = "" + animate = true + } + } + else -> { + if (pin == pinNumber.value) { + autoShowKeyboard = false + pinNumber.value = pin + onClick(true) + } else { + pin = "" // clear pin when not correct + animate = true + } + } + } + }, + autoShowKeyboard = autoShowKeyboard + ) + } + } + + Spacer(Modifier.weight(1f)) + + // Cancel - Button + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.px.dp) + ) { + Spacer(Modifier.weight(1f)) + + // Cancel - Button + TextButton( + modifier = Modifier + .size(width=80.px.dp, height=40.px.dp), + onClick = { + Timber.d("Cancel Clicked") + onClick(false) + } + ) { + Text( + text = stringResource(R.string.cancel), + style = RobotoTypography.titleMedium, + fontSize = 15.px.sp, + color = Color.Black + ) + } + + // OK - Button + TextButton( + modifier = Modifier + .size(width=68.px.dp, height=40.px.dp), + onClick = { + Timber.d("OK Clicked: ${pin}") + when (lockState) { + LockNavigation.Enter -> { + if (pin.length == 6) { + pinNumber.value = pin + onClick(true) + } else { + pin = "" + animate = true + } + } + else -> { + if (pin == pinNumber.value) { + pinNumber.value = pin + onClick(true) + } else { + pin = "" // clear pin when not correct + animate = true + } + } + } + } + ) { + Text( + text = stringResource(R.string.ok), + style = RobotoTypography.titleMedium, + fontSize = 15.px.sp, + color = Color.Black + ) + } + } + + Spacer(Modifier.weight(1f)) + } + + Spacer(modifier = Modifier.weight(1f)) + } + } +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/main/AlertView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/main/AlertView.kt new file mode 100644 index 0000000..2f833bc --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/main/AlertView.kt @@ -0,0 +1,181 @@ +package com.laseroptek.raman.ui.screens.main + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.AlertType +import com.laseroptek.raman.ui.common.fullscreen.FullScreen +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.dropShadow +import com.laseroptek.raman.utils.ext.px + +@Composable +fun AlertView( + type: AlertType = AlertType.WARNING, + title: String = stringResource(R.string.error_title), + description: String= "The system temperature is very high", + onClick: () -> Unit = {} +) { + FullScreen( + contentAlignment = Alignment.Center, + backgroundColor = Color.Transparent + ) { + Column( + modifier = Modifier + .size(500.px.dp, 280.px.dp) + .clip(RoundedCornerShape(28.px.dp)) + .background(Color(0,0,0).copy(0.75f)) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0,0,0).copy(alpha = 0.2f), + blur = 48.px.dp, + offsetX = 0.px.dp, + offsetY = 24.px.dp, + spread = 0.px.dp + ) + .dropShadow( + shape = RoundedCornerShape(28.px.dp), + color = Color(0,0,0).copy(alpha = 0.1f), + blur = 6.px.dp, + offsetX = 0.px.dp, + offsetY = 3.px.dp, + spread = 0.px.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Column( + modifier = Modifier + .size(470.px.dp, 250.px.dp) + .clip(RoundedCornerShape(20.px.dp)) + .border(2.px.dp, color = Color(120, 123, 130), shape = RoundedCornerShape(28.px.dp)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // close button + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.px.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.Bottom + ) { + // Close + if (type != AlertType.ERROR) { + IconButton( + onClick = { onClick.invoke() }, + modifier = Modifier.size(30.px.dp, 30.px.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, + modifier = Modifier.size(24.px.dp), + contentScale = ContentScale.Crop, + colorFilter = ColorFilter.tint(Color.White) + ) + } + Spacer(modifier = Modifier.width(16.px.dp)) + } + } + + // logo + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + Image( + painter = painterResource(id = R.drawable.ic_warning), + contentDescription = null, + modifier = Modifier.size(50.px.dp, 40.px.dp), + contentScale = ContentScale.Crop, + colorFilter = ColorFilter.tint(Color(236,1,1)) + ) + } + + // title + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = RobotoTypography.bodyMedium, + fontWeight = FontWeight.ExtraLight, + fontSize = 30.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + letterSpacing = (3).sp + ) + } + + // description + Row( + modifier = Modifier + .fillMaxWidth() + .height(80.px.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = description, + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.ExtraLight, + fontSize = 20.px.sp, + color = Color.White, + textAlign = TextAlign.Center, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + } + } + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1280dp,height=720dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewAlertView() { + AlertView( + type = AlertType.WARNING, + title = "Warning", + description = "The system temperature is very high", + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainScreen.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainScreen.kt new file mode 100644 index 0000000..91ef01e --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainScreen.kt @@ -0,0 +1,139 @@ +package com.laseroptek.raman.ui.screens.main + +import android.content.res.Configuration +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.navigation.navigationItemsLists +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber +import com.laseroptek.raman.R + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalMaterial3Api::class) +@Composable +fun MainScreen( + mainNavController: NavHostController = rememberNavController(), + mainViewModel: MainViewModel = hiltViewModel() +) { + val isInitialized by mainViewModel.isInitialized.collectAsState() + val keyboardController = LocalSoftwareKeyboardController.current + + DisposableEffect(Unit) { + onDispose { + // This is good practice, but not the main fix. + // It hides the keyboard when this screen is left. + keyboardController?.hide() + } + } + + // It requests focus for the root composable and then hides the keyboard. + val view = LocalView.current + LaunchedEffect(Unit) { + view.post { + view.clearFocus() // Clear focus from any text field that might have grabbed it. + keyboardController?.hide() + } + } + + /* + val navBackStackEntry by mainNavController.currentBackStackEntryAsState() + val currentRoute by remember(navBackStackEntry) { + derivedStateOf { + navBackStackEntry?.destination?.route + } + } + */ + + // Use Crossfade to prevent the 90-frame layout "snap" + Crossfade(targetState = isInitialized, label = "init_fade") { ready -> + if (!ready) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) + } else { + Scaffold( + topBar = { + Spacer(Modifier.height(45.px.dp)) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + content = { innerPadding -> + Timber.d("padding: $innerPadding") + Box(modifier = Modifier.fillMaxSize()) { + MainView( + mainNavController = mainNavController, + innerPadding = innerPadding, + mainViewModel = mainViewModel, + items = navigationItemsLists, + onItemClick = { currentNavigationItem -> + mainNavController.navigate(currentNavigationItem.route) { + mainNavController.graph.startDestinationRoute?.let { startDestinationRoute -> + popUpTo(startDestinationRoute) { + saveState = true + } + } + launchSingleTop = true + restoreState = true + } + }, + ) + } + } + ) + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun MainScreenPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + MainScreen( + mainViewModel = mainViewModel + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainTopBar.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainTopBar.kt new file mode 100644 index 0000000..fa12637 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainTopBar.kt @@ -0,0 +1,131 @@ +package com.laseroptek.raman.ui.screens.main + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.laseroptek.raman.R +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import com.laseroptek.raman.utils.ext.noRippleClickable +import com.laseroptek.raman.utils.ext.px + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainTopBar( + mainViewModel: MainViewModel = hiltViewModel(), + onClickWarning: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + //val handPiece by mainViewModel.handPiece.collectAsState() + //val handPieceTitle = handPieceTypes.get(handPiece.type) + val warning by mainViewModel.warning.collectAsState() + + Column { + Row( + modifier = modifier + .fillMaxWidth() + .height(45.px.dp) + .padding(start = 30.px.dp, end = 25.px.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.logo), + contentDescription = null, + modifier = Modifier + .size(120.px.dp, 26.px.dp), + ) + + /* + Spacer(Modifier.width(30.px.dp)) + + Text( + text = handPieceTitle, + style = RobotoTypography.titleMedium, + fontSize = 16.px.sp, + lineHeight = 26.px.sp, + letterSpacing = 0.92.px.sp, + fontWeight = FontWeight.Normal, + color = Color(255,255,255), + textAlign = TextAlign.Center + ) + */ + + Spacer(Modifier.weight(1f)) + + if (warning.code != 0) { + Image( + painter = painterResource(id = R.drawable.ic_alert), + contentDescription = null, + modifier = Modifier + .noRippleClickable(onClick = onClickWarning) + .size(45.px.dp), + ) + } + + /* + Text( + text = Date().formatToServerDateDefaults(), + style = PretendardTypography.titleSmall, + fontSize = 30.px.sp, + color = Color("#54000000".toColorInt()) + ) + + Spacer(Modifier.width(38.px.dp)) + + Text( + text = Date().formatToServerTimeDefaults(), + style = PretendardTypography.titleSmall, + fontSize = 30.px.sp, + color = Color("#54000000".toColorInt()) + ) + */ + + //Spacer(Modifier.width(57.px.dp)) + } + + //BottomShadow(alpha = .15f, height = 8.px.dp) + } +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + device = "spec:width=1280dp,height=720dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun MainTopBarPreview( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + MainTopBar( + mainViewModel = mainViewModel + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainView.kt new file mode 100644 index 0000000..6502cb7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainView.kt @@ -0,0 +1,274 @@ +package com.laseroptek.raman.ui.screens.main + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.R +import com.laseroptek.raman.const.AlertType +import com.laseroptek.raman.const.ERROR_TABLE +import com.laseroptek.raman.const.WARNING_TABLE +import com.laseroptek.raman.data.datasource.db.RamanDatabase +import com.laseroptek.raman.data.source.db.RamanDatabaseService +import com.laseroptek.raman.navigation.NavigationItem +import com.laseroptek.raman.navigation.NavigationSideBar +import com.laseroptek.raman.navigation.Routes +import com.laseroptek.raman.navigation.graphs.MainNavGraph +import com.laseroptek.raman.navigation.navigationItemsLists +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.ui.screens.config.engineer.EngineerPasswordPopup +import com.laseroptek.raman.ui.screens.config.language.LanguageView +import com.laseroptek.raman.ui.screens.config.time.TimeSettingPopup +import com.laseroptek.raman.ui.screens.home.count.ResetLaserCountPopup +import com.laseroptek.raman.ui.screens.home.dcd.DcdSettingPopup +import com.laseroptek.raman.ui.screens.home.energy.EnergyDetectPopup +import com.laseroptek.raman.ui.screens.home.preset.PresetLoadPopup +import com.laseroptek.raman.ui.screens.home.preset.PresetSavePopup +import com.laseroptek.raman.ui.screens.home.standby.StandByPopup +import com.laseroptek.raman.utils.DefaultDispatcherProvider +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainView( + mainNavController: NavHostController = rememberNavController(), + //currentRoute: String? = null, + innerPadding: PaddingValues = PaddingValues(0.dp), + mainViewModel: MainViewModel = hiltViewModel(), + items: List = navigationItemsLists, + onItemClick: (NavigationItem) -> Unit = {}, +) { + val navBackStackEntry by mainNavController.currentBackStackEntryAsState() + val currentRoute by remember(navBackStackEntry) { + derivedStateOf { + navBackStackEntry?.destination?.route + } + } + + val warning by mainViewModel.warning.collectAsState() + val error by mainViewModel.error.collectAsState() + + Box { + // Layer 1: Full Background Image (Bottom-most layer) + Image( + painter = painterResource(id = R.drawable.background), + contentDescription = "Background Image", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + + // Layer 2: Main Screen Content + // This is the main content area, it should be drawn before the UI controls. + MainNavGraph( + mainNavController = mainNavController, + innerPadding = innerPadding, + mainViewModel = mainViewModel, + ) + + // Layer 3: Left Navigation Bar (On top of MainNavGraph) + // Hide on Engineer and Lock screens + if (currentRoute != Routes.Engineer.route && currentRoute != Routes.Lock.route) { + NavigationSideBar( + items = items, + currentRoute = currentRoute, + innerPadding = innerPadding, + onItemClick = onItemClick, + modifier = Modifier + .align(Alignment.CenterStart) + ) + } + + // Layer 4: TopBar (On top of everything so far) + if (currentRoute != Routes.Lock.route) { + MainTopBar( + mainViewModel = mainViewModel, + onClickWarning = { + mainViewModel.showWarningPopup = true + } + ) + } + + // androidx.constraintlayout.helper.widget.Layer 5 & 6: Modal Popups (Top-most layers) + // These should always be last so they appear on top of all other UI. + // Remember to wrap them in FullScreen to block background touches. + + /////////////////////////////////////////////////////////////////////////////////////////// + // Popups + + // Error over TopBar + if (error.code != 0) { + AlertView( + type = AlertType.ERROR, + title = stringResource(R.string.error_title), + description = stringResource( + ERROR_TABLE[ error.code ] + ), + onClick = { + Timber.d("Convert to run: ${error.code}") + } + ) + } + + // Warning over TopBar + else if (mainViewModel.showWarningPopup) { + AlertView( + type = AlertType.WARNING, + title = stringResource(R.string.warning_title), + description = stringResource( + WARNING_TABLE [ warning.code ] + ), + onClick = { + mainViewModel.showWarningPopup = false + Timber.d("showWarningPopup: ${mainViewModel.showWarningPopup}") + } + ) + } + + else if (mainViewModel.showEnergyDetectPopup) { + EnergyDetectPopup( + title = stringResource(R.string.enery_check_title), + mainViewModel = mainViewModel, + onClick = { + mainViewModel.showEnergyDetectPopup = false + } + ) + } + + else if (mainViewModel.showPresetLoadPopup) { + PresetLoadPopup( + title = stringResource(R.string.load_title), + mainViewModel = mainViewModel, + /* + presetList = mainViewModel.presetList, + //selectedPresetIndex = mainViewModel.tmpSelectedPresetIndex, + */ + onClick = { + mainViewModel.showPresetLoadPopup = false + } + ) + } + + else if (mainViewModel.showPresetSavePopup) { + PresetSavePopup( + mainViewModel = mainViewModel, + onClick = { ok -> + // reset selected preset index + if (ok) { + mainViewModel.setSelectedPresetIndex(0) + } + mainViewModel.showPresetSavePopup = false + } + ) + } + + else if (mainViewModel.showDcdSettingPopup) { + DcdSettingPopup( + mainViewModel = mainViewModel, + onDismiss = { + mainViewModel.showDcdSettingPopup = false + } + ) + } + + else if (mainViewModel.showResetLaserCountPopup) { + ResetLaserCountPopup( + title = stringResource(R.string.reset_laser_count_title), + onClick = { okValue -> + Timber.d("ResetLaserCountPopup: $okValue") + if (okValue) { + mainViewModel.setLaserCount(0) + //mainViewModel.saveLaserCountToPreference() + } + mainViewModel.showResetLaserCountPopup = false + } + ) + } + + else if (mainViewModel.showStandByPopup) { + StandByPopup( + //mainViewModel = mainViewModel, + //description = stringResource(R.string.stand_by_message), + onClick = { + mainViewModel.txLaserStatusEntry(0x53) // send standBy + + mainViewModel.showStandByPopup = false + } + ) + } + + else if (mainViewModel.showTimeSettingPopup) { + TimeSettingPopup( + onDismiss = { + mainViewModel.showTimeSettingPopup = false + } + ) + } + + else if (mainViewModel.showEngineerPasswordPopup) { + EngineerPasswordPopup( + title = stringResource(R.string.engineer_mode_password), + onClick = { result -> + if (result) { + //mainViewModel.showEngineerModePopup.value = true + mainNavController.navigate(Routes.Engineer.route) + } + mainViewModel.showEngineerPasswordPopup = false + } + ) + } + + else if (mainViewModel.showLanguageScreen) { + LanguageView( + //showLanguageScreen = showLanguageScreen, + //mainViewModel = mainViewModel + onClick = { ok -> + mainViewModel.showLanguageScreen = false + } + ) + } + } +} + + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun PreviewMain( + mainViewModel:MainViewModel = MainViewModel( + preferenceRepository = PreferenceRepository(LocalContext.current), + serialPortRepository = SerialPortRepository.createWithFakeRepository(), + databaseRepository = DatabaseRepository(RamanDatabaseService(RamanDatabase.getInstance(LocalContext.current))), + dispatcherProvider = DefaultDispatcherProvider(), + applicationContext = LocalContext.current + ) +) { + mainViewModel.showEnergyDetectPopup = false + MainView( + mainViewModel = mainViewModel, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt new file mode 100644 index 0000000..7638625 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt @@ -0,0 +1,2289 @@ +package com.laseroptek.raman.ui.screens.main + +import android.content.Context +import android.media.AudioManager +import android.media.MediaPlayer +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.laseroptek.raman.BuildConfig +import com.laseroptek.raman.R +import com.laseroptek.raman.const.CMD +import com.laseroptek.raman.const.CMD.CMD_FLAG +import com.laseroptek.raman.const.CMD.WRITE_FLAG +import com.laseroptek.raman.const.DEFAULT_GUIDE_BEAM +import com.laseroptek.raman.const.EnergyTable_10_10 +import com.laseroptek.raman.const.EnergyTable_12_12 +import com.laseroptek.raman.const.EnergyTable_3_15 +import com.laseroptek.raman.const.EnergyTable_5_5 +import com.laseroptek.raman.const.EnergyTable_7_7 +import com.laseroptek.raman.const.Fluences +import com.laseroptek.raman.const.HzTable_10_10 +import com.laseroptek.raman.const.HzTable_12_12 +import com.laseroptek.raman.const.HzTable_3_15 +import com.laseroptek.raman.const.HzTable_5_5 +import com.laseroptek.raman.const.HzTable_7_7 +import com.laseroptek.raman.const.KEY_YELLOW +import com.laseroptek.raman.const.LASER_STATUS +import com.laseroptek.raman.const.LaserParameter +import com.laseroptek.raman.const.MAX_CHART_COLUMN_DATA_SIZE +import com.laseroptek.raman.const.MAX_GUIDE_BEAM +import com.laseroptek.raman.const.MAX_LAMP_COUNT +import com.laseroptek.raman.const.MAX_LOG_COUNT +import com.laseroptek.raman.const.MIN_GUIDE_BEAM +import com.laseroptek.raman.const.MinMaxUpDownState +import com.laseroptek.raman.const.PresetList +import com.laseroptek.raman.const.PulseDurations +import com.laseroptek.raman.const.READ_WRITE +import com.laseroptek.raman.const.RX_TIMEOUT_THRESHOLD +import com.laseroptek.raman.const.RepetitionsByColorKey +import com.laseroptek.raman.const.SERIAL_BAUDRATE +import com.laseroptek.raman.const.SERIAL_PORT_NAME +import com.laseroptek.raman.const.SHOW_ENERGY_DETECT +import com.laseroptek.raman.const.SprayDcdList +import com.laseroptek.raman.const.UpDownState +import com.laseroptek.raman.const.VoltageTable +import com.laseroptek.raman.const.txRepeatMillis +import com.laseroptek.raman.data.model.DcdType +import com.laseroptek.raman.data.model.LifeTime +import com.laseroptek.raman.data.model.Preset +import com.laseroptek.raman.data.model.Refer2 +import com.laseroptek.raman.data.model.SerialResult +import com.laseroptek.raman.data.model.serial.Alert +import com.laseroptek.raman.data.model.serial.DcdGas +import com.laseroptek.raman.data.model.serial.DcdGasOnOff +import com.laseroptek.raman.data.model.serial.EnergyControl +import com.laseroptek.raman.data.model.serial.EnergyControlStop +import com.laseroptek.raman.data.model.serial.EnergyHandpiece +import com.laseroptek.raman.data.model.serial.EnergyHandpieceRemove +import com.laseroptek.raman.data.model.serial.EnergyMeasured +import com.laseroptek.raman.data.model.serial.EnergyVersion +import com.laseroptek.raman.data.model.serial.GuideBeam +import com.laseroptek.raman.data.model.serial.HandPiece +import com.laseroptek.raman.data.model.serial.LaserStatus +import com.laseroptek.raman.data.model.serial.Oven +import com.laseroptek.raman.data.model.serial.Packet +import com.laseroptek.raman.data.model.serial.PurgeBubble +import com.laseroptek.raman.data.model.serial.QSwitch +import com.laseroptek.raman.data.model.serial.SprayDcd +import com.laseroptek.raman.data.model.serial.Temperature +import com.laseroptek.raman.data.model.serial.TemperatureTimeStamp +import com.laseroptek.raman.data.model.serial.Version +import com.laseroptek.raman.data.source.db.model.SerialLog +import com.laseroptek.raman.repository.DatabaseRepository +import com.laseroptek.raman.repository.PreferenceRepository +import com.laseroptek.raman.repository.SerialPortRepository +import com.laseroptek.raman.utils.DispatcherProvider +import com.laseroptek.raman.utils.FixedSizeLIFOQueue +import com.laseroptek.raman.utils.SystemSound.setCurrentSystemVolume +import com.laseroptek.raman.utils.ext.degreeToStep +import com.laseroptek.raman.utils.ext.getCmdString +import com.laseroptek.raman.utils.ext.getHexString +import com.laseroptek.raman.utils.ext.getKey2ListForKey1 +import com.laseroptek.raman.utils.ext.getValue +import com.laseroptek.raman.utils.ext.mask +import com.laseroptek.raman.utils.ext.printHex +import com.laseroptek.raman.utils.ext.setVoltageTableValue +import com.laseroptek.raman.utils.ext.stepToDegree +import com.laseroptek.raman.utils.ext.toByteArray +import com.laseroptek.raman.utils.ext.toCS +import com.laseroptek.raman.utils.ext.toDcdGas +import com.laseroptek.raman.utils.ext.toEnergyControl +import com.laseroptek.raman.utils.ext.toEnergyHandpiece +import com.laseroptek.raman.utils.ext.toEnergyMeasured +import com.laseroptek.raman.utils.ext.toEnergyVersion +import com.laseroptek.raman.utils.ext.toError +import com.laseroptek.raman.utils.ext.toFluenceMutableStateFlowMap +import com.laseroptek.raman.utils.ext.toHandPiece +import com.laseroptek.raman.utils.ext.toHpShotCount +import com.laseroptek.raman.utils.ext.toInt +import com.laseroptek.raman.utils.ext.toLaserStatus +import com.laseroptek.raman.utils.ext.toMutableStateFlow +import com.laseroptek.raman.utils.ext.toOven +import com.laseroptek.raman.utils.ext.toPacketResult +import com.laseroptek.raman.utils.ext.toPurgeBubble +import com.laseroptek.raman.utils.ext.toQSwitch +import com.laseroptek.raman.utils.ext.toSprayDcd +import com.laseroptek.raman.utils.ext.toTemperature +import com.laseroptek.raman.utils.ext.toVersion +import com.laseroptek.raman.utils.ext.toVoltageMutableStateFlowMap +import com.laseroptek.raman.utils.ext.toWarning +import com.laseroptek.raman.utils.ext.toYYYYMMDDHHMMSSString +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import kotlin.experimental.or +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + +@HiltViewModel +class MainViewModel @Inject constructor( + private val preferenceRepository: PreferenceRepository, + private val serialPortRepository: SerialPortRepository, + private val databaseRepository: DatabaseRepository, + private val dispatcherProvider: DispatcherProvider, + private val applicationContext: Context, +) : ViewModel() { + + /////////////////////////////////////////////////////////////////////////////////f/////////////// + // MainActivity - + + // apkUpdateEvent + private val _apkUpdateEvent = MutableSharedFlow() + val apkUpdateEvent: SharedFlow = _apkUpdateEvent.asSharedFlow() + suspend fun emitUpdateApkEvent() { // EngineerMode - onUpdateClick + Timber.d("apkUpdateEvent emit") + _apkUpdateEvent.emit(Unit) // Emit an event + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // show Popups + var showEnergyDetectPopup by mutableStateOf(SHOW_ENERGY_DETECT) + + var showWarningPopup by mutableStateOf(false) + var showResetLaserCountPopup by mutableStateOf(false) + var showPresetLoadPopup by mutableStateOf(false) + var showPresetSavePopup by mutableStateOf(false) + var showDcdSettingPopup by mutableStateOf(false) + var showStandByPopup by mutableStateOf(false) + var showEngineerPasswordPopup by mutableStateOf(false) + var showLanguageScreen by mutableStateOf(false) + var showTimeSettingPopup by mutableStateOf(false) + + //////////////////////////////////////////////////////////////////////////////////////////////// + // MainScreen - + + // initialization flag (to prevent too much work on its main thread on initialize) + private val _isInitialized = MutableStateFlow(false) + val isInitialized = _isInitialized.asStateFlow() + fun setInitialized(value: Boolean) { _isInitialized.value = value } + + // voltageTable - (PulseWidth(ms), Energy(J)) -> Voltage(V) + // - Engineer Mode에서 Energy 값 변경 시 - voltageTable 변경 및 Preference 저장 + // - Main 화면 StandBy 선택시, laser Status 값에 전달할 Voltage 계산시 사용 됨. + // - 앱 실행시 저장된 테이블을 preference 에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값) + private val _voltageTable: MutableStateFlow, MutableStateFlow>> = + VoltageTable.toVoltageMutableStateFlowMap() + val voltageTable = _voltageTable.asStateFlow() + fun setVoltageTable(v: MutableMap, MutableStateFlow>) { + _voltageTable.value = v + v.forEach { + _voltageTable.value.setVoltageTableValue(it.key, it.value.value) + } + } + fun getVoltage(pulseWidth: Float, energy: Float): Int { + val t = _voltageTable.value + val v = t.getValue(pulseWidth, energy)?.value ?: 0 + return v + } + + + // energyTable - (PulseWidth(ms), Fluence(J/cm²)) -> Energy(J) + // - Handpiece 타입에 따라 각각의 Energy Table로 구분 + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 energyTable 업데이트 + // - 앱 실행시 저장된 테이블에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값) + private val _energyTable: MutableStateFlow, Float>> = + EnergyTable_5_5.toFluenceMutableStateFlowMap() + val energyTable = _energyTable.asStateFlow() + fun setEnergyTable(e: MutableMap, Float>) { + _energyTable.value = e + } + + // fluenceList - handpiece type에 따른 Fluence 단계 값 (Guage 단계값 테이블) + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 energyTable 업데이트 + // - Fluences는 5_5 일때의 목록 값 (초기값: handPeiece none 상태) + // - ref. Repetitions, PulseDurations는 fixed Table 임 + private val _fluenceList: MutableStateFlow> = + Fluences.toMutableStateFlow() + val fluenceList = _fluenceList.asStateFlow() + fun setFluenceList(f: List) { + _fluenceList.value = f + } + + // hzTable - (PulseWidth(ms), Fluence(J/cm²)) -> hzType(int) + // - Handpiece 타입에 따라 각각의 Energy Table로 구분 + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 hzTable 업데이트 + // - 앱 실행시 저장된 테이블에서 값 해당 값 로딩 (없으면 기본값 = 상수값 = 공장초기화 값) + private val _hzTable: MutableStateFlow, Int>> = + HzTable_5_5.toFluenceMutableStateFlowMap() + val hzTable = _hzTable.asStateFlow() + fun setHzTable(h: MutableMap, Int>) { + _hzTable.value = h + } + + // repetitionList - handpiece type에 따른 Repetition 단계 값 (Guage 단계값 테이블) + // - Handpiece 연결시, 시리얼 명령으로 수신된 handpiece type에 따라 hzTable 업데이트 + // - Repetition은 5_5 일때의 목록 값 (초기값: handPeiece none 상태) + // - ref. PulseDurations는 fixed Table 임 + private val _repetitionList: MutableStateFlow> = + RepetitionsByColorKey[KEY_YELLOW]!!.toMutableStateFlow() + val repetitionList = _repetitionList.asStateFlow() + fun setRepetitionList(r: List) { + _repetitionList.value = r + } + + // pulseAngle, fluenceAngle, repetitionAngle - Laser Slider (Guage) + // - 메인 화면의 Guage 표시. + // - 현재 설정된 각각의 값들의 단계별 각도 값을 유지 한다. + + private val _pulseAngle: MutableStateFlow = MutableStateFlow(0f) + val pulseAngle = _pulseAngle.asStateFlow() + fun setPulseAngle(a: Float) { _pulseAngle.value = a } + + private val _fluenceAngle: MutableStateFlow = MutableStateFlow(0f) + val fluenceAngle = _fluenceAngle.asStateFlow() + fun setFluenceAngle(a: Float) { _fluenceAngle.value = a } + + private val _repetitionAngle: MutableStateFlow = MutableStateFlow(0f) + val repetitionAngle = _repetitionAngle.asStateFlow() + fun setRepetitionAngle(a: Float) { _repetitionAngle.value = a } + + // Define the custom comparator + private val presetComparator = Comparator { a, b -> + val aIsZero = a.priority == 0 + val bIsZero = b.priority == 0 + + when { // If a's priority is 0 and b's is not, a comes after b. + aIsZero && !bIsZero -> 1 + // If b's priority is 0 and a's is not, a comes before b. + !aIsZero && bIsZero -> -1 + // Otherwise (both are 0 or both are non-zero), sort by priority, + // then by handPieceType as a secondary sort. + else -> { + val priorityCompare = a.priority.compareTo(b.priority) + if (priorityCompare != 0) { + priorityCompare + } else { + a.handPieceType.compareTo(b.handPieceType) + } + } + } + } + + private val _presetList = MutableStateFlow>( + // Initialize with a deep copy of PresetList, sorted as per your original logic + PresetList.map { it.copy() } + .sortedWith(presetComparator) // Primary sort by priority, secondary by handPieceType + ) + val presetList = _presetList + fun setPresetList(list: List = PresetList) { + // deep copy + _presetList.value = list + .map{ it.copy() } + } + fun setPresetListSorted(list: List = PresetList) { + // deep copy + _presetList.value = list + .map{ it.copy() } + .sortedWith(presetComparator) + } + fun deletePreset(index: Int) { + val currentList = _presetList.value.toMutableList() + if (index >= 0 && index < currentList.size) { + currentList.removeAt(index) + //_presetList.value = currentList // Assign new list to StateFlow + setPresetList(currentList) + } else { + Timber.e("deletePreset: Invalid index $index for list size ${currentList.size}") + } + } + fun addPreset(preset: Preset) { + val currentList = _presetList.value.toMutableList() + currentList.add(preset.copy()) // Add a copy to ensure immutability if preset is reused + // Re-sort if adding can change order, or decide on insertion logic + _presetList.value = currentList.sortedWith(presetComparator) + } + fun updatePresetName(index: Int, newName: String) { + val currentList = _presetList.value.toMutableList() + if (index >= 0 && index < currentList.size) { + val updatedPreset = currentList[index].copy(name = newName) + currentList[index] = updatedPreset + //_presetList.value = currentList // Assign new list to StateFlow + setPresetList(currentList) + Timber.d("Updated preset at index $index with name: $newName. List: ${_presetList.value}") + } else { + Timber.e("updatePresetName: Invalid index $index for list size ${currentList.size}") + } + } + + // selectedPresetIndex - 선택된 preset button Index + // - 초기값 = 0 (un selected) + private val _selectedPresetIndex: MutableStateFlow = MutableStateFlow(0) + val selectedPresetIndex = _selectedPresetIndex.asStateFlow() + fun setSelectedPresetIndex(i: Int) { _selectedPresetIndex.value = i } + + // sprayDcdList - Spreay DCD Option List (6 Fixed Size List) + var sprayDcdList = SprayDcdList.toMutableStateList() + fun setSprayDcdList(list: List) { + sprayDcdList = list.map{ it.copy() }.toMutableStateList() + } + fun setSprayDcd(idx: Int, item: SprayDcd) { + sprayDcdList[idx] = item.copy() + } + + // selectedSprayDcdIndex - 메인 화면 DCD { Spray | Delay } + // - DcdSettingPopup에서 선택한 sprayDcdList index + private val _selectedSprayDcdIndex: MutableStateFlow = MutableStateFlow(1) + val selectedSprayDcdIndex = _selectedSprayDcdIndex.asStateFlow() + fun setSelectedSprayDcdIndex(i: Int) { _selectedSprayDcdIndex.value = i } + + // laserCount - # of laser count + private val _laserCount = MutableStateFlow(0) + val laserCount = _laserCount.asStateFlow() + fun setLaserCount(c: Int) { _laserCount.value = c } + fun increaseLaserCount() { _laserCount.value++ } + + // lampCount - tot # of laser count (reset not allowed) + private val _lampCount = MutableStateFlow(0) + val lampCount = _lampCount.asStateFlow() + fun setLampCount(c: Int) { _lampCount.value = c } + fun increaseLampCount() { + if (_lampCount.value < MAX_LAMP_COUNT) { + _lampCount.value++ + } else { + Timber.w("Lamp count has reached its maximum limit of $MAX_LAMP_COUNT.") + txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, byteArrayOf(LASER_STATUS.STAND_BY.toByte())) + + // laser 상태 초기화 (stand by: 0x53) + setLaserStatus(laserStatus.value.copy ( + laserStatus = 0x53 + )) + } + } + + // dcdCount - # of dcd cooler shot + private val _dcdCount = MutableStateFlow(0) + val dcdCount = _dcdCount.asStateFlow() + fun setDcdCount(c: Int) { _dcdCount.value = c } + fun increaseDcdCount() { _dcdCount.value++ } + + // hpCount - # of hp count + private val _hpCount = MutableStateFlow(999000) + val hpCount = _hpCount.asStateFlow() + fun setHpCount(c: Int) { _hpCount.value = c } + fun increaseHpCount() { _hpCount.value++ } + + fun getHPCount(): Int { + val remainingCount = when (handPiece.value.type) { + 1 -> lifeTime.value.hp5x5 - hpCount.value + 2 -> lifeTime.value.hp7x7 - hpCount.value + 3 -> lifeTime.value.hp10x10 - hpCount.value + 4 -> lifeTime.value.hp12x12 - hpCount.value + 5 -> lifeTime.value.hp3x15 - hpCount.value + else -> return 0 // Return 6 spaces if the type is unknown + } + + return remainingCount.coerceAtLeast(0) + } + + fun getHPCountForMainScreen(): String { + val remainingCount = when (handPiece.value.type) { + 2 -> lifeTime.value.hp7x7 - hpCount.value + 3 -> lifeTime.value.hp10x10 - hpCount.value + 4 -> lifeTime.value.hp12x12 - hpCount.value + 5 -> lifeTime.value.hp3x15 - hpCount.value + else -> lifeTime.value.hp5x5 - hpCount.value + } + + // Ensure the result is not negative for display + val displayCount = maxOf(0, remainingCount) + + if (displayCount > 1000) { + return "%4d K".format(displayCount/1000) // 1000 + } else { + return "%4d".format(displayCount) + } + } + + // lifeTime - Reserved for future use (Int[8]) + private val _lifeTime: MutableStateFlow = MutableStateFlow(LifeTime()) + val lifeTime = _lifeTime.asStateFlow() + fun setLifeTime(t: LifeTime) { + _lifeTime.value = t + } + + // dcdType - DCD Can Type & LifeSpan + // - Engineer Mode에서 설정하고, 해당 값으로 DCD Settings에서 Expected Value 계산함. + private val _dcdType: MutableStateFlow = MutableStateFlow(DcdType()) + val dcdType = _dcdType.asStateFlow() + fun setDcdType(d: DcdType) { + _dcdType.value = d + } + val gasChargeRate: StateFlow = combine(_dcdCount, _dcdType) { count, type -> + val canFactor = if (type.canType == 700) 1 else 2 + val totalCapacity = canFactor * (6000 * type.lifeSpan) + + val gasUsageRate = if (totalCapacity > 0) { + count.toFloat() / totalCapacity.toFloat() + } else { + 0f + } + + val chargeRate = 100f - (gasUsageRate * 100f) // Ensure it's a percentage + + Timber.d("Recalculated gasChargeRate: $chargeRate (Count: $count, Type: ${type.canType})") + chargeRate.coerceIn(0f, 100f) // Keep between 0 and 100 + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = 100f + ) + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Info Screen (Shared) + + private val _chartDataQueue = + MutableStateFlow(FixedSizeLIFOQueue(MAX_CHART_COLUMN_DATA_SIZE)) + val chartDataQueue = _chartDataQueue.asStateFlow() + fun setChartDataQueue(data: FixedSizeLIFOQueue) { + _chartDataQueue.value = data + } + fun addChartDataListQueue(dataList: List) { + _chartDataQueue.value.addAll(dataList) + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Engineer Screen (Shared) + + // serialLogs + private val _serialLogList: MutableStateFlow> = + MutableStateFlow>(emptyList()) + val serialLogList = _serialLogList.asStateFlow() + fun setSerialLogList(list: List) { + _serialLogList.value = list.toList() // Creates a new list instance + } + + // apkFile + private val _apkFile: MutableState = mutableStateOf("") + val apkFile: MutableState = mutableStateOf("") + fun setApkFile(file: String) { + _apkFile.value = file + } + + // productSerialList + private val _productSerialList: MutableStateFlow> = MutableStateFlow(listOf("B","U","O","C","L","D")) + val productSerialList = _productSerialList + fun setProductSerialList(list: List) { + _productSerialList.value = list.toList() // Creates a new list instance + } + + // laserHandSerialList + val _laserHandSerialList: MutableStateFlow> = MutableStateFlow(listOf("B","U","O","C","L","D")) + val laserHandSerialList = _laserHandSerialList + fun setLaserHandSerialList(list: List) { + _laserHandSerialList.value = list.toList() // Creates a new list instance + } + + // powerSupplySerialList + val _powerSupplySerialList: MutableStateFlow> = MutableStateFlow(listOf("B","U","O","C","L","D")) + val powerSupplySerialList = _powerSupplySerialList + fun setPowerSupplySerialList(list: List) { + _powerSupplySerialList.value = list.toList() // Creates a new list instance + } + + val lastCheckRealTimeMillis: MutableState = mutableStateOf(System.currentTimeMillis()) + + //////////////////////////////////////////////////////////////////////////////////////////////// + // CONFIG - + + private val _sliderVolume = MutableStateFlow(0f) // Keep the MutableStateFlow private + val sliderVolume = _sliderVolume.asStateFlow() // Expose an immutable StateFlow + fun setSliderVolume(volume: Float) { + viewModelScope.launch { + _sliderVolume.value = volume + } + } + + // guideBeam (Slider) + private val _guideBeam = MutableStateFlow(DEFAULT_GUIDE_BEAM.toFloat()) + val guideBeam = _guideBeam.asStateFlow() + fun setGuideBeam(value: Float) { + _guideBeam.value = value + } + + // guideBeamMin + private val _guideBeamMin = MutableStateFlow(MIN_GUIDE_BEAM) + val guideBeamMin = _guideBeamMin.asStateFlow() + fun setGuideBeamMin(value: Int) { + _guideBeamMin.value = value + } + + // guideBeamMax + private val _guideBeamMax = MutableStateFlow(MAX_GUIDE_BEAM) + val guideBeamMax = _guideBeamMax.asStateFlow() + fun setGuideBeamMax(value: Int) { + _guideBeamMax.value = value + } + suspend fun onGuideBeamChanged(state: MinMaxUpDownState) { + Timber.d("onGuideBeamChanged: $state") + + val currentMin = guideBeamMin.value + val currentMax = guideBeamMax.value + + when (state) { + MinMaxUpDownState.MinDown -> if (currentMin >= 10) { + setGuideBeamMin(currentMin - 10) + } + MinMaxUpDownState.MinUp -> if (currentMin < MAX_GUIDE_BEAM && (currentMin + 10) < currentMax) { + setGuideBeamMin(currentMin + 10) + } + MinMaxUpDownState.MaxDown -> if (currentMax >= 10 && (currentMin + 10) < currentMax) { + setGuideBeamMax(currentMax - 10) + } + MinMaxUpDownState.MaxUp -> if (currentMax < MAX_GUIDE_BEAM) { + setGuideBeamMax(currentMax + 10) + } + MinMaxUpDownState.MinLongDown -> if (currentMin >= 100) { + setGuideBeamMin(currentMin - 100) + } else if (currentMin >= 10) { + setGuideBeamMin(currentMin - 10) + } + MinMaxUpDownState.MinLongUp -> if (currentMin < (MAX_GUIDE_BEAM - 100) && (currentMin + 100) < currentMax) { + setGuideBeamMin(currentMin + 100) + } else if (currentMin < MAX_GUIDE_BEAM && (currentMin + 10) < currentMax) { + setGuideBeamMin(currentMin + 10) + } + MinMaxUpDownState.MaxLongDown -> if (currentMax > 100 && (currentMin + 100) < currentMax) { + setGuideBeamMax(currentMax - 100) + } else if (currentMax >= 10 && (currentMin + 10) < currentMax) { + setGuideBeamMax(currentMax - 10) + } + MinMaxUpDownState.MaxLongUp -> if (currentMax < (MAX_GUIDE_BEAM - 100)) { + setGuideBeamMax(currentMax + 100) + } else if (currentMax < MAX_GUIDE_BEAM) { + setGuideBeamMax(currentMax + 10) + } + } + + // Save the updated values to preferences + saveGuideBeamMinToPreference() + saveGuideBeamMaxToPreference() + + // After updating the state, send the packet + val newMin = guideBeamMin.value // get the potentially updated value + val newMax = guideBeamMax.value // get the potentially updated value + val guideBeam = guideBeam.value.toInt() + + /* + val value = when(guideBeamValue) { + 0 -> 0 + 1 -> newMin + 10 -> newMax + else -> (newMin + (guideBeamValue - 1) * ((newMax - newMin) / 9)) + } + */ + val value = (newMin + (guideBeam - 1) * ((newMax - newMin) / 9)) + + Timber.d("guideBeam: $value, guideBeam: $guideBeam, guideBeamMax: $newMax, guideBeamMin: $newMin") + txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = value)) + } + + private val _pulseType = MutableStateFlow(1) // 0: Single, 1: Repeat + val pulseType = _pulseType.asStateFlow() + fun setPulseType(t: Int) { + _pulseType.value = t + } + + private val _opTimeHour: MutableStateFlow = MutableStateFlow(0L) + val opTimeHour = _opTimeHour.asStateFlow() + fun setOpTimeHour(h: Long) { + _opTimeHour.value = h + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Tx Repeate + private val _txRepeatCount: MutableStateFlow = MutableStateFlow(0) + val txRepeatCount = _txRepeatCount.asStateFlow() + fun setTxRepeatCount(v: Int) { + _txRepeatCount.value = v + } + fun increaseTxRepeatCount() { + setTxRepeatCount(txRepeatCount.value + 1) + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // SERIAL - PACKET + + // laserStatus + private val _laserStatus: MutableStateFlow = MutableStateFlow(LaserStatus()) + val laserStatus = _laserStatus.asStateFlow() + fun setLaserStatus(laserStatus: LaserStatus) { + _laserStatus.value = laserStatus + } + + // handPience + private val _handPiece: MutableStateFlow = MutableStateFlow(HandPiece()) + val handPiece = _handPiece.asStateFlow() + fun setHandPiece(handPiece: HandPiece) { + _handPiece.value = handPiece + } + + // warning + private val _warning: MutableStateFlow = MutableStateFlow(Alert(0)) + val warning = _warning.asStateFlow() + fun setWarning(w: Alert) { + _warning.value = w + } + + // error + private val _error: MutableStateFlow = MutableStateFlow(Alert(0)) + val error = _error.asStateFlow() + fun setError(e: Alert){ + _error.value = e + } + + // version + private val _version: MutableStateFlow = MutableStateFlow(Version()) + val version = _version.asStateFlow() + fun setVersion(v: Version) { + _version.value = v + } + + // purge bubble + private val _purgeBubble: MutableStateFlow = MutableStateFlow(PurgeBubble()) + val purgeBubble: MutableStateFlow = _purgeBubble + fun setPurgeBubble(p: PurgeBubble) { + _purgeBubble.value = p + } + + // dcd gas + private val _dcdGas: MutableStateFlow = MutableStateFlow(DcdGas()) + val dcdGas: MutableStateFlow = _dcdGas + fun setDcdGas(d: DcdGas) { + _dcdGas.value = d + } + + // spray dcd + private val _sprayDcd: MutableStateFlow = MutableStateFlow(SprayDcd()) + val sprayDcd: MutableStateFlow = _sprayDcd + fun setSprayDcd(s: SprayDcd) { + _sprayDcd.value = s + } + + // temperature (READ) + private val _temperature: MutableStateFlow = MutableStateFlow(Temperature()) + val temperature = _temperature.asStateFlow() + fun setTemperature(t: Temperature) { + _temperature.value = t + } + + // temperature (WRITE - EngineerMode) + private val _temperature_write: MutableStateFlow = MutableStateFlow(Temperature()) + val temperature_write = _temperature_write.asStateFlow() + fun setTemperatureWrite(t: Temperature) { + _temperature_write.value = t + } + + // oven + private val _oven: MutableStateFlow = MutableStateFlow(Oven()) + val oven: MutableStateFlow = _oven + fun setOven(o: Oven) { + _oven.value = o + } + + // q swtich + private val _qSwitch: MutableStateFlow = MutableStateFlow(QSwitch()) + val qSwitch: MutableStateFlow = _qSwitch + fun setQSwitch(q: QSwitch) { + _qSwitch.value = q + } + + // q swtich sub + private val _qSwitchSub: MutableStateFlow = MutableStateFlow(QSwitch()) + val qSwitchSub: MutableStateFlow = _qSwitchSub + fun setQSwitchSub(q: QSwitch) { + _qSwitchSub.value = q + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Energy detect + + // Energy Detect - Refer 2. (Engineer Mode) + private val _energyDetectRefer2: MutableStateFlow = MutableStateFlow(Refer2()) + val energyDetectRefer2 = _energyDetectRefer2.asStateFlow() + fun setEnergyDetectRefer2(value: Refer2) { + _energyDetectRefer2.value = value + } + + // EnergyVersion (Energy Detect - Version) + private val _energyVersion: MutableStateFlow = MutableStateFlow(EnergyVersion()) + val energyVersion: MutableStateFlow = _energyVersion + fun setEnergyVersion(v: EnergyVersion) { + _energyVersion.value = v + } + + // EnergyHandpiece (Energy Detect - Check if insert in mount) + private val _energyHandpiece: MutableStateFlow = MutableStateFlow(EnergyHandpiece()) + val energyHandpiece: MutableStateFlow = _energyHandpiece + fun setEnergyHandpiece(h: EnergyHandpiece) { + _energyHandpiece.value = h + } + + // EnergyControl (Energy Detect - Control to detect energy) + private val _energyControl: MutableStateFlow = MutableStateFlow(EnergyControl()) + val energyControl: MutableStateFlow = _energyControl + fun setEnergyControl(c: EnergyControl) { + _energyControl.value = c + } + + // EnergyMeasured (Energy Detect - Transmit the measured data) + private val _energyMeasured: MutableStateFlow = MutableStateFlow(EnergyMeasured()) + val energyMeasured: MutableStateFlow = _energyMeasured + fun setEnergyMeasured(m: EnergyMeasured) { + _energyMeasured.value = m + } + + // EnergyMeasured Write (Energy Detect - Write value from Engineer Mode) + private val _energyMeasuredWrite: MutableStateFlow = MutableStateFlow(EnergyMeasured()) + val energyMeasuredWrite: MutableStateFlow = _energyMeasuredWrite + fun setEnergyMeasuredWrite(m: EnergyMeasured) { + _energyMeasuredWrite.value = m + } + + // rcvd data + private var _rcvdData: ByteArray = byteArrayOf() + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Play Beep Sound + + private var mediaPlayer: MediaPlayer? = null + private var beepJob: Job? = null + + suspend fun playBeepSound() { + if (beepJob?.isActive == true) { + Timber.d( "MediaPlayer isActive") + return + } + + beepJob = viewModelScope.launch(dispatcherProvider.io) { + try { + if (mediaPlayer == null) { + mediaPlayer = MediaPlayer.create(applicationContext, R.raw.sound) + } + + mediaPlayer?.let { player -> + Timber.d("MediaPlayer playing") + player.seekTo(0) // Start from beginning + player.start() + + val delayMillis = 50L // player.duration.toLong() + Timber.d("delayMillis: $delayMillis") + + delay(delayMillis) + } + } catch (e: Exception) { + Timber.e(e, "MediaPlayer error") + mediaPlayer?.release() + mediaPlayer = null + } + } + + beepJob?.join() + } + + override fun onCleared() { + super.onCleared() + beepJob?.cancel() // Stop any pending beep delay + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + } + + fun txPacketOnce() { + // viewModel init 으로 이동. 필요. + // 경고 정보 조회 (주기적 heart beat) + // repeatOnLifecycle은 Activity가 포그라운드에 있을 때로 한정지어, 특정 Lifecycle이 Trigger 되었을 때 동작하도록 만드는 block 임. + + // tx Version Read + txPacket(READ_WRITE.READ, CMD.VERSION, byteArrayOf(0x41.toByte())) + + // tx Q-Switch Write + txPacket(READ_WRITE.WRITE, CMD.Q_SWITCH, qSwitch.value) + + // tx Guide Beam Write + txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = guideBeam.value.toInt())) + + // tx DCD_GAS Write (DEFAULT VALUE) + txPacket(READ_WRITE.WRITE, CMD.DCD_GAS, dcdGas.value.copy(status = 0x50)) + + // tx SPRAY_DCD Write (DEFAULT VALUE) + txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, sprayDcd.value) + } + + // Example: Emitting an event after a delay + fun txPacketLoop() { + viewModelScope.launch { + // viewModel init 으로 이동. 필요. + // 경고 정보 조회 (주기적 heart beat) + // repeatOnLifecycle은 Activity가 포그라운드에 있을 때로 한정지어, 특정 Lifecycle이 Trigger 되었을 때 동작하도록 만드는 block 임. + + while (true) { + val noOfRepeat = if (showEnergyDetectPopup) { + 2 + } else { + 5 + } + + delay(txRepeatMillis) + + /* + if (error.value.code != 0) { + Timber.e("error.value.code: ${error.value.code}") + continue + } + */ + + // hand piece connection check + if (txRepeatCount.value % noOfRepeat == 0) + txPacket(READ_WRITE.READ, CMD.HAND_PIECE, byteArrayOf()) + + // hand piece mount (detector) + // - Once handpiece is mounted (it receives 0x47 (Good)) don't send again + // - If Unmounted, F/W send EnergyHandpiece result again. + else if (txRepeatCount.value % noOfRepeat == 1) { + if (energyHandpiece.value.status == 0x47) { + Timber.d("Handpiece mount OK.. don't send packet again") + } else { + txPacket( + READ_WRITE.READ, + CMD.ENERGY_DETECT, + EnergyHandpiece(type = 0x02, status = 0x41) + ) + } + } + + // hp shot count + else if (txRepeatCount.value % noOfRepeat == 2) + txPacket(READ_WRITE.READ, CMD.HP_SHOT_COUNT, byteArrayOf()) + + // warning + else if (txRepeatCount.value % noOfRepeat == 3) + txPacket(READ_WRITE.READ, CMD.WARNING, byteArrayOf()) + + // temperature + else if (txRepeatCount.value % noOfRepeat == 4) { + txPacket(READ_WRITE.READ, CMD.TEMPERATURE, byteArrayOf()) + } + + + // q switch (GUI save : data source) + // txPacket(READ_WRITE.READ, CMD.Q_SWITCH, byteArrayOf()) + + // error (event type) + // txPacket(READ_WRITE.READ, CMD.ERROR, byteArrayOf()) + + // opTime - update system operation time (total uptime in hour) + updateHourlyUptime() + + // update tx repeat count + increaseTxRepeatCount() + } + } + } + + private suspend fun updateHourlyUptime() { + val currentRealTimeMillis = System.currentTimeMillis() + val elapsedSinceLastCheckMillis = currentRealTimeMillis - lastCheckRealTimeMillis.value + + val newFullHoursPassed = elapsedSinceLastCheckMillis.milliseconds.toLong(DurationUnit.HOURS) + //Timber.d("Elapsed since last check: ${elapsedSinceLastCheckMillis.milliseconds}, New full hours: $newFullHoursPassed") + + if (newFullHoursPassed > 0) { + setOpTimeHour( opTimeHour.value + newFullHoursPassed ) + saveOpTimeHourToPreference() + Timber.d("Updated total uptime: ${opTimeHour.value} hours. Last check updated to: $newFullHoursPassed") + + lastCheckRealTimeMillis.value = currentRealTimeMillis + } else { + //Timber.d("Not a full hour passed since last check.") + } + } + + private val _isCommunicationStable = MutableStateFlow(true) + val isCommunicationStable = _isCommunicationStable.asStateFlow() + + private var lastResponseTime = System.currentTimeMillis() + + fun rxPacketLoop() { + viewModelScope.launch(dispatcherProvider.io) { + serialPortRepository.open(SERIAL_PORT_NAME, SERIAL_BAUDRATE) + .catch { e -> + // DETECT: Open Fail or Port Crash + Timber.e("Serial Port Open Fail: ${e.message}") + _isCommunicationStable.value = false + }.collect { + //isLoading.value = true; _isLoadingFlow.value = true; + if (it is SerialResult.Success) { + _isCommunicationStable.value = true + lastResponseTime = System.currentTimeMillis() // Reset timeout + + // 수신 패킷 데이터 Hex 덤프 (로깅) + /* + if (BuildConfig.DEBUG) { + it.data.printHex("RX") + } + */ + + // Combine the persistent buffer with the new data + val combinedData = _rcvdData + it.data + + // Find all complete packets and get the leftover data + val (foundPackets, remainedData) = findCompletePackets(combinedData) + + // Update the persistent buffer with the new leftover data for the next cycle + _rcvdData = remainedData + + // Process each complete packet that was found + if (foundPackets.isNotEmpty()) { + Timber.d("Found ${foundPackets.size} complete packets. Processing...") + foundPackets.forEach { packetBytes -> + val result: SerialResult = packetBytes.toPacketResult() + val packet: Packet? = if (result is SerialResult.Success) result.data else null + + if (packet != null) { + procRxPacket(packet) + } else { + Timber.e("Packet parsing error for:") + } + } + } + + // Log remaining data if any + if (_rcvdData.isNotEmpty()) { + Timber.d("Remained data (${_rcvdData.size} bytes) saved for next read.") + } + } else if (it is SerialResult.Error) { + Timber.e("error: %s", it.exception.message) + _isCommunicationStable.value = false + + if (!BuildConfig.DEBUG) { + setError(Alert(code = 1)) // PSU/COMM error + } + + //serialPortRepository.close() + //delay(5 * 1000L) // 5 초 간격으로 재오픈 시도 + //rxPacketLoop() + } + + //isLoading.value = false; _isLoadingFlow.value = false; + } + } + + // Start a Watchdog Coroutine to detect "No Response" + monitorConnectionTimeout() + } + + private fun monitorConnectionTimeout() { + viewModelScope.launch(dispatcherProvider.io) { + while (isActive) { + val currentTime = System.currentTimeMillis() + if (currentTime - lastResponseTime > RX_TIMEOUT_THRESHOLD) { + if (isCommunicationStable.value) { + Timber.w("Serial Timeout: No response for ${RX_TIMEOUT_THRESHOLD}ms") + _isCommunicationStable.value = false + + if (!BuildConfig.DEBUG) { + setError(Alert(code = 1)) // PSU/COMM error + } + } + } + delay(500) // Check every half second + } + } + } + + /** + * Splits a ByteArray into a list of complete packets (from STX to ETX) and leftover data. + * A packet is defined by STX (0x21) and ETX (0x0d). + * + * @param data The combined buffer of old and new data to search through. + * @return A Pair containing a List of complete packet ByteArrays and the remaining ByteArray. + */ + private fun findCompletePackets(data: ByteArray): Pair, ByteArray> { + val packets = mutableListOf() + var searchArea = data + val stx = 0x21.toByte() + val etx = 0x0d.toByte() + + while (true) { + val stxIndex = searchArea.indexOf(stx) + if (stxIndex == -1) { + // No STX found in the remaining data, so all of it is leftover. + return Pair(packets, searchArea) + } + + // Search for ETX *after* the found STX + val etxIndex = searchArea.drop(stxIndex).indexOf(etx) + if (etxIndex == -1) { + // Found an STX but no corresponding ETX. The rest of the data is a partial packet. + // Keep the data from STX onwards as the remainder. + val remaining = searchArea.sliceArray(stxIndex..searchArea.lastIndex) + return Pair(packets, remaining) + } + + // Found a complete packet. The actual ETX index in the original searchArea. + val absoluteEtxIndex = stxIndex + etxIndex + val completePacket = searchArea.sliceArray(stxIndex..absoluteEtxIndex) + packets.add(completePacket) + + // The next search should start right after the found packet. + searchArea = searchArea.sliceArray((absoluteEtxIndex + 1)..searchArea.lastIndex) + } + } + + + private fun procRxPacket(packet: Packet) { + viewModelScope.launch { + when ( packet.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) ) { + CMD.VERSION -> { + val v = packet.data.toVersion() + setVersion(v.copy()) + Timber.d("VERSION: ${v}") + } + CMD.LASER_STATUS -> { + val l = packet.data.toLaserStatus() + setLaserStatus(l.copy()) + // on receuve laser on (or interval) increase count and save to pref. + // Laser On 패킷만 counting 하도록 변경 (2개 패킷 모두 수신 됨) + if (_laserStatus.value.laserStatus == LASER_STATUS.LASER_ON /* || l.laserStatus == LASER_STATUS.INTERVAL */) { + playBeepSound() + + increaseLaserCount() + increaseHpCount() + + increaseLampCount() + saveLampCountToPreference() + + val repetitionIndex = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size) + val repetition = repetitionList.value[repetitionIndex] + Timber.d("repetition: ${repetition}") + if (repetition < 6.0f) { + increaseDcdCount() + saveDcdCountToPreference() + } + } + + val hexLaserStatus = String.format("laserStatus: %02X", _laserStatus.value.laserStatus) + Timber.d("LASER_STATUS: ${hexLaserStatus}") + Timber.d("laserCount: ${laserCount.value}") + Timber.d("lampCount: ${lampCount.value}") + } + CMD.PURGE_BUBBLE -> { + val p = packet.data.toPurgeBubble() + setPurgeBubble(p.copy()) + Timber.d("PURGE_BUBBLE: ${p}") + } + CMD.DCD_GAS -> { + val d = packet.data.toDcdGas() + setDcdGas(d.copy()) + Timber.d("DCD_GAS: ${d}") + + /* laser 발진 (on)시 증가 (6Hz 미만시) + if (d.status == DCD_STATUS.ON) { // ox41 == ON + ++_dcdCount.value + saveDcdCountToPreference() + } + */ + } + CMD.SPRAY_DCD -> { + val s = packet.data.toSprayDcd() + setSprayDcd(s.copy()) + + /* + setDcdCount( dcdCount.value + 1 ) + */ + Timber.d("SPRAY_DCD: ${s}") + } + CMD.HAND_PIECE -> { + val h = packet.data.toHandPiece() + + // check handPiece is changed + if (h.type == handPiece.value.type) { + Timber.d("handPiece is not changed") + return@launch + } + + setHandPiece(h.copy()) + Timber.d("HAND_PIECE: ${h}") + + // laser Count 초기화 + setLaserCount(0) + + // send STANDBY packet + txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, byteArrayOf(LASER_STATUS.STAND_BY.toByte())) + + // laser 상태 초기화 (stand by: 0x53) + setLaserStatus(laserStatus.value.copy ( + laserStatus = 0x53 + )) + + // init energy detect + setEnergyHandpiece(EnergyHandpiece(0x02, 0x00)) + setEnergyControl(EnergyControl(0x03, 0x00)) + setEnergyMeasured(EnergyMeasured(0x04, 0x00, 0x00)) + + // hand piece 별 count 증가 + when (h.type) { + 1 -> { // hp 5x5 + setLifeTime( + _lifeTime.value.copy( + hp5x5 = _lifeTime.value.hp5x5 + 1 + ) + ) + saveLifeTimeToPreference() + Timber.d("_lifeTime (hp5x5): ${_lifeTime}") + } + 2 -> { + setLifeTime( + _lifeTime.value.copy( + hp7x7 = _lifeTime.value.hp7x7 + 1 + ) + ) + saveLifeTimeToPreference() + Timber.d("_lifeTime (hp7x7): ${_lifeTime}") + } + 3 -> { + setLifeTime( + _lifeTime.value.copy( + hp10x10 = _lifeTime.value.hp10x10 + 1 + ) + ) + saveLifeTimeToPreference() + Timber.d("_lifeTime (hp10x10): ${_lifeTime}") + } + 4 -> { + setLifeTime( + _lifeTime.value.copy( + hp12x12 = _lifeTime.value.hp12x12 + 1 + ) + ) + saveLifeTimeToPreference() + Timber.d("_lifeTime (hp12x12): ${_lifeTime}") + } + 5 -> { + setLifeTime( + _lifeTime.value.copy( + hp3x15 = _lifeTime.value.hp3x15 + 1 + ) + ) + saveLifeTimeToPreference() + Timber.d("_lifeTime (hp3x15): ${_lifeTime}") + } + else -> { + Timber.d("unknwon hand piece type: ${h.type}") + return@launch + } + } + + // hand piece가 변경 시점 에서, pref 테이블 값을 -> energyTable 로 로딩 + loadFluenceTable(handPiece.value.type) + + // parameter angle initialize + setFluenceAngle(0f) + setRepetitionAngle(0f) + setPulseAngle(0f) + + // fluenceList 초기화 (현재 pulse duration 값에 해당하는 목록) + val pulseWidthStep = 0 + val pulseWidth = PulseDurations[pulseWidthStep] // preset.value.pulseWidth + val newFluenceList = energyTable.value.getKey2ListForKey1(pulseWidth) + + setFluenceList(newFluenceList) + Timber.d("newFluenceList: ${newFluenceList}") + + // repetitionList 초기화. + loadHzTable( handPiece.value.type ) + + val hzType = hzTable.value.getValue(pulseWidth, newFluenceList.get(0)) + Timber.d("hzType: ${hzType}") + val list = RepetitionsByColorKey.get(hzType)!! + setRepetitionList( list ) + Timber.d("newRepetitionList: ${_repetitionList.value}") + + //initialize preset to first value + //val fluenceStep = fluenceAngle.value.degreeToStep(totalSteps = fluenceList.value.size) + //val newFluenceDegree = fluenceStep.stepToDegree(totalSteps = fluenceList.value.size) + //setFluenceAngle( newFluenceDegree ) + } + CMD.HP_SHOT_COUNT -> { + val c = packet.data.toHpShotCount() + setHpCount(c.count) + + saveHpCountToPreference() + Timber.d("HP_SHOT_COUNT: ${hpCount.value}") + } + CMD.TEMPERATURE -> { + val t = packet.data.toTemperature() + + // DEBUG + Timber.d("BEFORE UPDATE:") + chartDataQueue.value.toList().sortedBy{ it.timestamp }.takeLast(25).forEachIndexed { idx, log -> + Timber.d("[%02d] %s (%s)".format(idx, log.timestamp.toYYYYMMDDHHMMSSString(), log.timestamp.toString())) + } + + // 1. get TemperatureTimeStamp from received temperature value + val newTemperaturePoint = TemperatureTimeStamp.fromTemperature(t, System.currentTimeMillis()) + Timber.d("[VM] ${newTemperaturePoint.timestamp.toYYYYMMDDHHMMSSString()} (${newTemperaturePoint.timestamp})") + + // 2. Get the current queue value once. + val currentQueue = chartDataQueue.value + + // 3. Create a new queue instance to ensure state is updated. + val updatedQueue = FixedSizeLIFOQueue(MAX_CHART_COLUMN_DATA_SIZE) + + // 4. Add all items from the current queue to the new one. + updatedQueue.addAll(currentQueue.toList()) + + // 5. Add ONLY the single new data point. + updatedQueue.add(newTemperaturePoint) + + // DEBUG + Timber.d("UPDATE QUEUE:") + updatedQueue.toList().sortedBy{ it.timestamp }.takeLast(25).forEachIndexed { idx, log -> + Timber.d("[%02d] %s (%s)".format(idx, log.timestamp.toYYYYMMDDHHMMSSString(), log.timestamp.toString())) + } + + // 6. Update the repository with the new state. + setChartDataQueue(updatedQueue) + + withContext(Dispatchers.Main) { + // 7. Update the UI with the new data. + setTemperature(t.copy()) + + // 8. Save Temperature to Preference + saveTemeratureToPreference() + + when { + temperature.value.ktp > temperature_write.value.ktp -> { + Timber.e("temperature.value.ktp > temperature_write.value.ktp") + /* #74 - KTP의 Write 값은 설정을 하기 위한 값임 (Error는 PSU에서 패킷을 전송함) + if (!showEnergyDetectPopup) { + setError(Alert(code = 10)) // ktp error + } + */ + } + temperature.value.chamber1 > temperature_write.value.chamber1 -> { + Timber.e("temperature.value.chamber1 > temperature_write.value.chamber1") + if (!showEnergyDetectPopup) { + setError(Alert(code = 11)) // chamber error + } + } + temperature.value.chamber2 > temperature_write.value.chamber2 -> { + Timber.e("temperature.value.chamber2 > temperature_write.value.chamber2") + if (!showEnergyDetectPopup) { + setError(Alert(code = 11)) // chamber error + } + } + temperature.value.basePlate > temperature_write.value.basePlate -> { + Timber.e("temperature.value.basePlate > temperature_write.value.basePlate") + if (!showEnergyDetectPopup) { + setError(Alert(code = 12)) // basePlate error + } + } + temperature.value.water > temperature_write.value.water -> { + Timber.e("temperature.value.water > temperature_write.value.water") + if (!showEnergyDetectPopup) { + setError(Alert(code = 13)) // water error + } + } + } + } + + Timber.d("chartDataQueue.size: ${chartDataQueue.value.size}") + //Timber.d("update chartDataQueue.value: ${chartDataQueue.value}") + } + CMD.OVEN -> { + val o = packet.data.toOven() + setOven(o.copy()) + Timber.d("OVEN: ${oven.value}") + } + CMD.Q_SWITCH -> { + val q = packet.data.toQSwitch() + setQSwitch(q.copy()) + Timber.d("Q_SWITCH: ${qSwitch.value}") + } + CMD.ENERGY_DETECT -> { + val type = packet.data.sliceArray(0..0).toInt() + Timber.d("ENERGY_DETECT: ${type}") + when (type) { + 0x01 -> { + val v = packet.data.toEnergyVersion() + setEnergyVersion(v.copy()) + Timber.d("EnergyVersion: ${v}") + } + + 0x02 -> { + val h = packet.data.toEnergyHandpiece() + setEnergyHandpiece(h.copy()) + + // init energy control & measured + setEnergyControl(EnergyControl(0x03, 0x00)) + setEnergyMeasured(EnergyMeasured(0x04, 0x00, 0x00)) + + Timber.d("EnergyHandpiece: ${h}") + } + + 0x03 -> { + val c = packet.data.toEnergyControl() + setEnergyControl(c.copy()) + + // init energy measured + setEnergyMeasured(EnergyMeasured(0x04, 0x00, 0x00)) + + Timber.d("EnergyControl: ${c}") + } + + 0x04 -> { + val m = packet.data.toEnergyMeasured() + setEnergyMeasured(m.copy()) + + if (energyMeasured.value.measured < energyMeasuredWrite.value.measured * energyDetectRefer2.value.good) { + // Send back Good + txPacket( + rw = READ_WRITE.WRITE, + cmd = CMD.ENERGY_DETECT, + data = EnergyMeasured( + type = 0x04, + status = 0x47, // 'G': Good + ) + ) + + } else { + // Send Not Good + txPacket( + rw = READ_WRITE.WRITE, + cmd = CMD.ENERGY_DETECT, + data = EnergyMeasured( + type = 0x04, + status = 0x4E, // 'N': Not Good + ) + ) + } + Timber.d("EnergyMeasured: ${m}") + } + + else -> { // error } + } + } + } + CMD.WARNING -> { + val w = packet.data.toWarning() + setWarning(w.copy()) + Timber.d("WARNING: ${warning.value}") + } + CMD.ERROR -> { + val e = packet.data.toError() + setError(e.copy()) + Timber.d("ERROR: ${e}") + } + } + + // 3) add to serial log (memory) + //serialLogs.add(it.data.getHexString(TX_RX.RX)) + + // 4) add to serial log (db) + when ( packet.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) ) { + CMD.LASER_STATUS, CMD.TEMPERATURE, CMD.WARNING, CMD.ERROR -> { + val logToInsert = SerialLog( + ts = System.currentTimeMillis(), + cmd = packet.cmd.toInt().mask(CMD_FLAG) + .mask(WRITE_FLAG), + tx = false, + w = (packet.cmd.toInt().mask(WRITE_FLAG) != 0), + data = packet.toByteArray(), + desc = packet.toByteArray().getHexString() + ) + Timber.d("Attempting to insert SerialLog: $logToInsert") + withContext(Dispatchers.IO) { + try { + databaseRepository.insertSerialLog(serialLog = logToInsert) + Timber.d("SerialLog insertion successful.") + } catch (e: Exception) { + Timber.e(e, "Error inserting SerialLog") + } + } + } + } + + if (BuildConfig.DEBUG) { + val rwStr = if (packet.cmd.toInt().mask(WRITE_FLAG) != 0) "W" else "R" + val cmdStr = getCmdString(packet.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG)) + packet.toByteArray().printHex("RX", rwStr, cmdStr) + } + } + } + + fun txLaserStatusEntry(laserStatus: Int) { + viewModelScope.launch { + // 1. Main > Gauge : get P(ms), F(J/cm²) + val pulseWidthStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size) + val pulseWidth = PulseDurations[pulseWidthStep] // preset.value.pulseWidth + + val fluenceStep = fluenceAngle.value.degreeToStep(totalSteps = fluenceList.value.size) + val fluence = fluenceList.value.get(fluenceStep) //preset.value.fluence + + val repetitionStep = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size) + val repetition = if (pulseType.value == 0) { + 0.0f + } else { + repetitionList.value.get(repetitionStep) + } + Timber.d("pulseWidth: ${pulseWidth}, fluence: ${fluence}, repetition: ${repetition}") + + // 2. Energy Table : get energy(J) + val energy = energyTable.value[Pair(pulseWidth, fluence)] ?: 0f + + if (energy == 0f) { + Timber.e("energy == 0") + return@launch + } + + // eg. pulseWidth: 0.5, fluence: 2.2 -> energy: 0.55 + Timber.d("pulseWidth: ${pulseWidth}, fluence: ${fluence} -> energy: ${energy}") + + // 2. get voltage from Voltage Table with (J) + val v = calculateInterpolatedC(pulseWidth, energy) + Timber.d("v: ${v}") + + /* + Toast.makeText(applicationContext, + "(ms: %.1f, J/cm²: %.1f) -> (J: %.3f, V: %.3f)".format( + pulseWidth, fluence, energy, v + ), + Toast.LENGTH_SHORT + ).show() + */ + + Timber.d( + "(ms: %.1f, J/cm²: %.1f) -> (J: %.3f, V: %.3f)".format( + pulseWidth, fluence, energy, v + ) + ) + + // 3. conver it to Laser Status + val laserStatusPacket = LaserStatus( + laserStatus = laserStatus, + voltage = v.toInt(), + onTime = pulseWidth, + frequence = repetition // preset.value.repetition, + ) + + setLaserStatus(laserStatusPacket) + + txPacket(READ_WRITE.WRITE, CMD.LASER_STATUS, laserStatusPacket) + } + } + + private fun calculateInterpolatedC(pulseWidth: Float, energy: Float): Float { + // 1. Find the entries in the map where the first element of the key (first) matches the given pulseWidth + // from VoltageTable (eg. 0.5f --> listOf(Pair(0.5f,0.5f), Pair(0.5f,1.5f), ...)) + // where, volatageTable is Map, Voltage(V)> + val v = voltageTable.value + val PulseWidthrelevantEntries = v.filterKeys { it.first == pulseWidth } + Timber.d("PulseWidthrelevantEntries : $PulseWidthrelevantEntries") + /* + // eg. pulseWidth: 0.5, energy: 0.55 + // Map, Voltage(V)> + Pair(0.5f,0.5f) to 440, + Pair(0.5f,1.5f) to 450, + Pair(0.5f,3f) to 460, + Pair(0.5f,4f) to 470, + Pair(0.5f,5f) to 480, + Pair(0.5f,6f) to 490, + Pair(0.5f,7f) to 500, + Pair(0.5f,8f) to 510, + Pair(0.5f,9f) to 520, + Pair(0.5f,10f) to 530, + Pair(0.5f,11f) to 540, + Pair(0.5f,12f) to 550, + */ + + // If no entries match 'pulseWidth', return 0 (its error should match at ) + if (PulseWidthrelevantEntries.isEmpty()) { + Timber.e("No entries found for pulseWidth = $pulseWidth") + return 0f + } + + // 2. Extract the (Energy(J), Voltage(V)) pairs from the relevant entries and sort them by first (pulseWidth) + val EnergyAndVoltages = PulseWidthrelevantEntries.map { + it.key.second to it.value.value.toFloat() + }.sortedBy { it.first } + Timber.d("EnergyAndVoltages : $EnergyAndVoltages") + + /* + // eg. pulseWidth: 0.5, energy: 0.55 + // Map + 0.5f to 440, + * <--- here + 1.5f to 450, + 3f to 460, + 4f to 470, + 5f to 480, + 6f to 490, + 7f to 500, + 8f to 510, + 9f to 520, + 10f to 530, + 11f to 540, + 12f to 550, + */ + + // 3. Find the first value (Energy(J)) same as given energy E(J) + val indexOfEnegrySameAsFirst = EnergyAndVoltages.indexOfFirst { it.first == energy } + if (indexOfEnegrySameAsFirst != -1) { + val (_: Float, v: Float) = EnergyAndVoltages[indexOfEnegrySameAsFirst] + Timber.d("Same Energy Value: return voltage of it ${v}") + return v + } + + // 4. else Find the first value (Energy(J)) greater than given energy E(J) + val indexOfEnegryGreaterFirst = EnergyAndVoltages.indexOfFirst { it.first > energy } + Timber.d("indexOfEnegryGreaterFirst = $indexOfEnegryGreaterFirst") // should be 1 in eg. pulseWidth: 0.5, energy: 0.55 + + // If no E(J) value is greater than given energy, it means given-energy is beyond the range of available E(J) values. + // should match any one so its error case. + if (indexOfEnegryGreaterFirst == -1) { + Timber.e("No E(J) found from VoltageTable for given energy (from Main> FluenceTable) = $energy") + return 0f + } + + // Get (e: Energy(J), v: Voltage(V)) + val (e2: Float, v2: Float) = EnergyAndVoltages[indexOfEnegryGreaterFirst] + val (e1: Float, v1: Float) = EnergyAndVoltages[indexOfEnegryGreaterFirst - 1] + + Timber.d("e1: ${e1}, e2: ${e2}") + Timber.d("v1: ${v1}, v2: ${v2}") + + // Calculate the interpolated Energy value using linear interpolation + // eg. pulseWidth: 0.5, energy: 0.55 + val interpolatedC = v1 + (v2 - v1) * ((energy - e1) / (e2 - e1)) + Timber.d("interpolatedC: ${interpolatedC}") + + return interpolatedC + } + + fun txPacket(rw: READ_WRITE, cmd: Int, data: T) { + viewModelScope.launch { + var txPacket = byteArrayOf() + var dataArray = byteArrayOf() + + //isLoading.value = true; _isLoadingFlow.value = true; + + if (rw == READ_WRITE.READ) { + //dataArray = if (cmd == CMD.VERSION || cmd == CMD.LASER_STATUS) byteArrayOf(0x41) else byteArrayOf() + dataArray = when(cmd) { + CMD.VERSION, CMD.LASER_STATUS -> byteArrayOf(0x41) + CMD.ENERGY_DETECT -> when(data) { + is EnergyVersion -> byteArrayOf(0x01) // handpiece version (R) + is EnergyHandpiece -> byteArrayOf(0x02, 0x41) // Check if insert in mount (R) + //is EnergyHandpieceRemove -> byteArrayOf(0x02, 0x42) // remove (W) + //is EnergyControl -> byteArrayOf(0x03, 0x41) // Start (W) + //is EnergyControlStop -> byteArrayOf(0x03, 0x42) // Stop (W) + //is EnergyMeasured -> byteArrayOf(0x04, 0x45) // measured (W) + else -> byteArrayOf() + } + else -> byteArrayOf() + } + txPacket = Packet( + stx = byteArrayOf(CMD.STX.toByte()), + cmd = byteArrayOf(cmd.toByte().or(CMD_FLAG.toByte())), + data = dataArray, + etx = byteArrayOf(CMD.ETX.toByte()) + ).toByteArray() + } else { + dataArray = when(data) { + //is EnergyVersion -> byteArrayOf(0x01) // handpiece version (R) + //is EnergyHandpiece -> byteArrayOf(0x02, 0x41) // Check if insert in mount (R) + is EnergyHandpieceRemove -> byteArrayOf(0x02, 0x42) // remove (W) + is EnergyControl -> data.toByteArray() //byteArrayOf(0x03, 0x41) // Start (W) + is EnergyControlStop -> byteArrayOf(0x03, 0x42) // Stop (W) + is EnergyMeasured -> byteArrayOf(0x04, 0x45) // measured (W) + is LaserStatus -> data.toByteArray() + is PurgeBubble -> data.toByteArray() + is DcdGas -> data.toByteArray() + is DcdGasOnOff -> data.toByteArray() + is SprayDcd -> data.toByteArray() + is GuideBeam -> data.toByteArray() + is Oven -> data.toByteArray() + is QSwitch -> data.toByteArray() + is ByteArray -> data + is String -> data.toByteArray() + is Int -> data.toString().toByteArray() + is Byte -> byteArrayOf(data) + else -> byteArrayOf() + } + + Timber.d("dataArray: ${dataArray}") + + txPacket = Packet( + stx = byteArrayOf(CMD.STX.toByte()), + cmd = byteArrayOf( + cmd.toByte().or(CMD_FLAG.toByte()).or(WRITE_FLAG.toByte()) + ), + data = dataArray, + cs = (byteArrayOf( + cmd.toByte().or(CMD_FLAG.toByte()).or(WRITE_FLAG.toByte()) + ) + dataArray).toCS(), + etx = byteArrayOf(CMD.ETX.toByte()) + ).toByteArray() + } + + if (BuildConfig.DEBUG) { + val rwStr = if (rw == READ_WRITE.READ) "R" else "W" + val cmdStr = getCmdString(cmd) + txPacket.printHex("TX", rwStr, cmdStr) + } + + //serialLogs.add(txPacket.getHexString(TX_RX.TX)) + + serialPortRepository.write(txPacket) + } + } + + fun close() { + serialPortRepository.close() + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Preference + + suspend fun saveHpCountToPreference() { + preferenceRepository.saveHpCountToPreference(hpCount.value) + } + + suspend fun loadHpCountFromPreference() { + val hpCountValue = preferenceRepository.getHpCountToPreference().first() + Timber.d("loadHpCountFromPreference: hpCountValue: ${hpCountValue}") + setHpCount(hpCountValue) + } + + suspend fun saveLampCountToPreference() { + preferenceRepository.saveLampCountToPreference(lampCount.value) + } + + suspend fun loadLampCountFromPreference() { + val v = preferenceRepository.getLampCountFromPreference().first() + Timber.d("loadLampCountFromPreference: v: ${v}") + setLampCount(v) + } + + suspend fun saveDcdCountToPreference() { + preferenceRepository.saveDcdCountToPreference(dcdCount.value) + } + + suspend fun loadDcdCountFromPreference() { + val v = preferenceRepository.getDcdCountFromPreference().first() + Timber.d("loadDcdCountFromPreference: v: ${v}") + setDcdCount(v) + } + + + suspend fun saveSliderVolumeToPreference() { + Timber.d("saveSliderVolumeToPreference: sliderVolume: ${sliderVolume.value}") + preferenceRepository.saveSliderVolumeToPreference( + sliderVolume.value + ) + } + + suspend fun initSliderVolumeFromPreference() { + val sliderVolume = preferenceRepository.getSliderVolumeFromPreference().first() + Timber.d("loadSliderVolumeFromPreference: sliderVolume: ${sliderVolume}") + setSliderVolume(sliderVolume) + + /* + withContext(Dispatchers.Main) { + setCurrentSystemVolume( + applicationContext, + AudioManager.STREAM_MUSIC, //AudioManager.STREAM_SYSTEM, + sliderVolume.toInt() + ) + } + */ + } + + suspend fun trimSerialLog() { + withContext(dispatcherProvider.io) { + try { + databaseRepository.trimLogs(limit = MAX_LOG_COUNT) + Timber.d("trimSerialLog successful.") + } catch (e: Exception) { + Timber.e(e, "trimSerialLog SerialLog") + } + } + } + + suspend fun saveGuideBeamToPreference() { + preferenceRepository.saveGuideBeamToPreference(guideBeam.value) + } + + suspend fun loadGuideBeamFromPreference() { + val guideBeam = preferenceRepository.getGuideBeamFromPreference().first() + Timber.d("loadGuideBeamFromPreference: guideBeam: ${guideBeam}") + setGuideBeam(guideBeam) + } + + suspend fun saveGuideBeamMinToPreference() { + preferenceRepository.saveGuideBeamMinToPreference(guideBeamMin.value) + } + + suspend fun loadGuideBeamMinFromPreference() { + val guideBeamMin = preferenceRepository.getGuideBeamMinFromPreference().first() + Timber.d("loadGuideBeamMinFromPreference: v: setGuideBeamMin($guideBeamMin)") + setGuideBeamMin(guideBeamMin) + } + + suspend fun saveGuideBeamMaxToPreference() { + preferenceRepository.saveGuideBeamMaxToPreference(guideBeamMax.value) + } + suspend fun loadGuideBeamMaxFromPreference() { + val guideBeamMax = preferenceRepository.getGuideBeamMaxFromPreference().first() + Timber.d("loadGuideBeamMaxFromPreference: v: setGuideBeamMax($guideBeamMax)") + setGuideBeamMax(guideBeamMax) + } + + suspend fun saveSprayDcdToPreference() { + preferenceRepository.saveSprayDcdToPreference(sprayDcd.value) + } + + suspend fun loadSprayDcdFromPreference() { + val sprayDcd = preferenceRepository.getSprayDcdFromPreference().first() + Timber.d("loadSprayDcdFromPreference: v: setSprayDcd($sprayDcd)") + setSprayDcd(sprayDcd) + } + + suspend fun saveQSwitchToPreference() { + preferenceRepository.saveQSwitchToPreference(qSwitch.value) + } + suspend fun loadQSwitchFromPreference() { + val qSwitch = preferenceRepository.getQSwitchFromPreference().first() + Timber.d("loadQSwitchFromPreference: v: setQSwitch($qSwitch)") + setQSwitch(qSwitch) + } + + suspend fun saveOpTimeHourToPreference() { + preferenceRepository.saveOpTimeHourToPreference(opTimeHour.value) + } + + suspend fun loadOpTimeHourFromPreference() { + val opTimeHourValue = preferenceRepository.getOpTimeHourFromPreference().first() + Timber.d("loadOpTimeHourFromPreference: v: setOpTimeHour($opTimeHourValue)") + setOpTimeHour(opTimeHourValue) + } + + suspend fun saveLifeTimeToPreference() { + preferenceRepository.saveLifeTimeToPreference(lifeTime.value) + } + + suspend fun loadLifeTimeFromPreference() { + val lifeTime = preferenceRepository.getLifeTimeFromPreference().first() + Timber.d("loadLifeTimeFromPreference: v: setLifeTime($lifeTime)") + setLifeTime(lifeTime) + } + + suspend fun saveTemeratureToPreference() { + preferenceRepository.saveTemperatureToPreference(temperature.value) + } + + suspend fun loadTemeratureFromPreference() { + val temperatureValue = preferenceRepository.getTemperatureFromPreference().first() + Timber.d("loadTemeratureFromPreference: v: setTemperature($temperatureValue)") + setTemperature(temperatureValue) + } + + suspend fun saveTemeratureWriteToPreference() { + preferenceRepository.saveTemperatureWriteToPreference(temperature_write.value) + } + + suspend fun loadTemeratureWriteFromPreference() { + val temperatureWriteValue = preferenceRepository.getTemperatureWriteFromPreference().first() + Timber.d("loadTemeratureWriteFromPreference: v: setTemperatureWrite($temperatureWriteValue)") + setTemperatureWrite(temperatureWriteValue) + } + + suspend fun saveEnergyMeasuredWriteToPreference() { + preferenceRepository.saveEnergyMeasuredWriteToPreference(energyMeasuredWrite.value) + } + suspend fun loadEnergyMeasuredWriteFromPreference() { + val energyMeasuredWrite = preferenceRepository.getEnergyMeasuredWriteFromPreference().first() + Timber.d("loadEnergyMeasuredWriteFromPreference: v: setEnergyMeasuredWrite($energyMeasuredWrite)") + setEnergyMeasuredWrite(energyMeasuredWrite) + } + + suspend fun saveEnergyRefer2ToPreference() { + preferenceRepository.saveEnergyRefer2ToPreference(energyDetectRefer2.value) + } + suspend fun loadEnergyRefer2FromPreference() { + val energyDetectRefer2 = preferenceRepository.getEnergyRefer2FromPreference().first() + Timber.d("loadEnergyRefer2FromPreference: v: setEnergyDetectRefer2($energyDetectRefer2)") + setEnergyDetectRefer2(energyDetectRefer2) + } + + suspend fun saveProductSerialListToPreference() { + preferenceRepository.saveProductSerialListToPreference(productSerialList.value) + } + suspend fun loadProductSerialListFromPreference() { + val productSerialList = preferenceRepository.getProductSerialListFromPreference().first() + Timber.d("loadProductSerialListFromPreference: v: setProductSerialList($productSerialList)") + setProductSerialList(productSerialList) + } + + suspend fun savePowerSupplySerialListToPreference() { + preferenceRepository.savePowerSupplySerialListToPreference(powerSupplySerialList.value) + } + suspend fun loadPowerSupplySerialListFromPreference() { + val powerSupplySerialList = preferenceRepository.getPowerSupplySerialListFromPreference().first() + Timber.d("loadPowerSupplySerialListFromPreference: v: setPowerSupplySerialList($powerSupplySerialList)") + setPowerSupplySerialList(powerSupplySerialList) + } + + suspend fun saveLaserHandSerialListToPreference() { + preferenceRepository.saveLaserHandSerialListToPreference(laserHandSerialList.value) + } + suspend fun loadLaserHandSerialListFromPreference() { + val laserHandSerialList = preferenceRepository.getLaserHandSerialListFromPreference().first() + Timber.d("loadLaserHandSerialListFromPreference: v: setLaserHandSerialList($laserHandSerialList)") + setLaserHandSerialList(laserHandSerialList) + } + + suspend fun savePresetListToPreference() { + preferenceRepository.savePresetListToPreference( + presetList.value + ) + } + + suspend fun loadSprayDcdIndexFromPreference() { + val index = preferenceRepository.getSprayDcdIndexFromPreference().first() + Timber.d("loadSprayDcdIndexFromPreference -> selectedSprayDcdIndex: $index") + setSelectedSprayDcdIndex(index) + } + + suspend fun saveSprayDcdIndexToPreference() { + preferenceRepository.saveSprayDcdIndexToPreference( + selectedSprayDcdIndex.value + ) + } + + suspend fun loadPresetListFromPreference() { + val presets = preferenceRepository.getPresetListFromPreference().first() + Timber.d("loadPresetListFromPreference: v: setPresetList($presets)") + setPresetList(presets) + } + + suspend fun saveSprayDcdListToPreference() { + preferenceRepository.saveSprayDcdList( + sprayDcdList.toMutableStateList() + ) + } + + suspend fun loadSprayDcdListFromPreference() { + val sprayDcdOptionListVal = preferenceRepository.getSprayDcdList().first() + Timber.d("loadSprayDcdListFromPreference: v: setSprayDcdList($sprayDcdOptionListVal)") + sprayDcdList = sprayDcdOptionListVal.toMutableStateList() + } + + suspend fun loadTemperatureDataFromDatabase() { + // Launch this on a background thread from the start + Timber.d("Executing loadTemperatureDataFromDatabase... (ONCE)") + + // Use .first() to read the data only once --- + withContext(dispatcherProvider.io) { + try { + val logs = databaseRepository.selectSerialLog(100).first() + + // All the processing now happens on this single, initial list of logs. + val temperatureLogs = logs.filter { + it.cmd.toInt().mask(CMD_FLAG).mask(WRITE_FLAG) == CMD.TEMPERATURE + } + + val distinctTemperatureLogs = temperatureLogs.distinctBy { + it.ts / 1000 + }.sortedBy { + it.ts + }.takeLast(MAX_CHART_COLUMN_DATA_SIZE) + + val historicalTemperatures = arrayListOf() + distinctTemperatureLogs.forEachIndexed { idx, log -> + val t = log.data.toTemperature() + val timestamp = log.ts + Timber.d( + "[%02d] %s (%s)".format( + idx, + timestamp.toYYYYMMDDHHMMSSString(), + timestamp.toString() + ) + ) + + val temperatureTimeStamp = + TemperatureTimeStamp.fromTemperature(t, timestamp) + historicalTemperatures.add( + temperatureTimeStamp + ) + } + + // DEBUG + historicalTemperatures.takeLast(25).forEachIndexed { idx, log -> + Timber.d( + "[%02d] %s (%s)".format( + idx, + log.timestamp.toYYYYMMDDHHMMSSString(), + log.timestamp.toString() + ) + ) + } + + // Switch to the main thread to update the StateFlow safely + withContext(dispatcherProvider.main) { + addChartDataListQueue(historicalTemperatures) + + // DEBUG + chartDataQueue.value.toList().takeLast(25).forEachIndexed { idx, log -> + Timber.d( + "[%02d] %s (%s)".format( + idx, + log.timestamp.toYYYYMMDDHHMMSSString(), + log.timestamp.toString() + ) + ) + } + } + + Timber.d("...Finished loading ${historicalTemperatures.size} historical temperature points into the queue.") + } catch (e: Exception) { + Timber.e("loadTemperatureDataFromDatabase -> %s", e.message) + } + } + } + + fun loadFluenceTable(handPieceType: Int) { + Timber.d("loadFluenceTable($handPieceType)") + + val energyTable = when (handPieceType) { + 2 -> EnergyTable_7_7 + 3 -> EnergyTable_10_10 + 4 -> EnergyTable_12_12 + 5 -> EnergyTable_3_15 + else -> EnergyTable_5_5 + } + + setEnergyTable( energyTable.mapValues { (_, fValue) -> fValue }.toMutableMap() ) + } + + fun loadHzTable(handPieceType: Int) { + Timber.d("loadHzTable($handPieceType)") + + val hzTable = when (handPieceType) { + 2 -> HzTable_7_7 + 3 -> HzTable_10_10 + 4 -> HzTable_12_12 + 5 -> HzTable_3_15 + else -> HzTable_5_5 + } + + setHzTable( hzTable.mapValues { (_, fValue) -> fValue }.toMutableMap() ) + } + + /** + * loadVoltageTableFromPreference (Skip - handPiece 변경 시점에서 해당 테이블 로딩) + * handPience 각각 Preference에 저장된 테이블 (Engineer) 설정 값을 로딩 함 + */ + suspend fun loadVoltageTableFromPreference() { + val v = preferenceRepository.getVoltageTableFromPreference().first() + val voltageTableMap = voltageTable.value + Timber.d("MM: loadVoltageTableFromPreference() begin") + v.forEach { (key, value) -> + val voltageTableVolFlow = voltageTableMap[key] + if (voltageTableVolFlow != null) { + voltageTableMap.setVoltageTableValue(key, value) + + //Timber.d("Eng: loadVoltageTableFromPreference() key: $key, value: $value") + } + } + Timber.d("MM: loadVoltageTableFromPreference() finish") + } + + suspend fun fetchSerialLog() { + Timber.d(".") + val logs = databaseRepository.selectSerialLog(100) + .flowOn(dispatcherProvider.io) + .catch { e -> + Timber.e("fetchSerialLog -> %s", e.message) + }.first() + + Timber.d("fetchSerialLog: ${logs}") + setSerialLogList(logs) + Timber.d("..") + } + + fun applyPreset(priority: Int) { + val preset = presetList.value.firstOrNull { it.handPieceType == handPiece.value.type && it.priority == priority } + Timber.d("preset: ${preset}") + + val newPreset = if (preset == null) { + Preset( + handPieceType = handPiece.value.type, + priority = priority, + fluence = energyTable.value.getKey2ListForKey1(0.5f).first(), + repetition = repetitionList.value.first(), + pulseWidth = PulseDurations.first(), + ) + } else { + preset + } + + val fluenceList = energyTable.value.getKey2ListForKey1(newPreset.pulseWidth) + + val pulseStep = PulseDurations.indexOf(newPreset.pulseWidth).takeIf {it != -1} ?: 0 + val fluenceStep = fluenceList.indexOf(newPreset.fluence).takeIf {it != -1} ?: 0 + val repetitionStep = repetitionList.value.indexOf(newPreset.repetition).takeIf {it != -1} ?: 0 + + Timber.d("pulseStep: ${pulseStep} fluenceStep: ${fluenceStep} repetitionStep: ${repetitionStep}") + + val pulseAngle = pulseStep.stepToDegree(totalSteps = PulseDurations.size) + val fluenceAngle = fluenceStep.stepToDegree(totalSteps = fluenceList.size) + val repetitionAngle = repetitionStep.stepToDegree(totalSteps = repetitionList.value.size) + + Timber.d("pulseStep: ${pulseStep} fluenceStep: ${fluenceStep} repetitionStep: ${repetitionStep}") + + setPulseAngle( pulseAngle ) + setFluenceAngle( fluenceAngle ) + setRepetitionAngle( repetitionAngle ) + + Timber.d("pulseAngle: ${pulseAngle}, fluenceAngle: ${fluenceAngle}, repetitionAngle: ${repetitionAngle}") + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Laser Parameter Control + //////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Centralized helper to update dependent laser parameters (Fluence list, Repetition list) + * when a primary parameter (like Pulse Duration or Fluence) changes. + */ + private fun updateLaserParameters(newPulseDuration: Float, newFluence: Float) { + // 1. Get the current repetition value BEFORE we change anything. + val repetitionStep = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size) + val currentRepetitionValue = repetitionList.value.getOrNull(repetitionStep) + + // 2. Update Fluence List based on the new Pulse Duration + val newFluenceList = energyTable.value.getKey2ListForKey1(newPulseDuration) + if (newFluenceList != fluenceList.value) { + setFluenceList(newFluenceList) + } + + // 3. Safely Update Repetition List (Prevents NullPointerException) + val newHzType = hzTable.value.getValue(newPulseDuration, newFluence) + val newRepetitionList = RepetitionsByColorKey[newHzType] ?: RepetitionsByColorKey[KEY_YELLOW]!! + if (newRepetitionList != repetitionList.value) { + setRepetitionList(newRepetitionList) + } + + // Smartly preserve Repetition Angle + // 4. Check if the old repetition value exists in the new list. + val oldRepetitionIndex = if (currentRepetitionValue != null) { + newRepetitionList.indexOf(currentRepetitionValue) + } else { + -1 + } + + if (oldRepetitionIndex != -1) { + // If the old value exists, set the slider to that position. + val preservedAngle = oldRepetitionIndex.stepToDegree(totalSteps = newRepetitionList.size) + setRepetitionAngle(preservedAngle) + Timber.d("Repetition value $currentRepetitionValue preserved at new angle $preservedAngle.") + } else { + // If it doesn't exist, THEN reset the slider to the beginning. + setRepetitionAngle(0f) + Timber.d("Repetition value $currentRepetitionValue not supported in new list. Resetting angle.") + } + + // 5. Conditionally reset the fluence angle slider if needed + /* + if (shouldResetFluenceAngle) { + setFluenceAngle(0f) + } + */ + + // 6. Any change invalidates the current preset selection. + setSelectedPresetIndex(0) + + Timber.d("Updated Laser Parameters: pulse=$newPulseDuration, fluence=$newFluence -> newRepListSize=${newRepetitionList.size}") + } + + /** + * This is the single entry point for the Up/Down buttons on the main screen's sliders. + * It correctly dispatches to the specific handlers for each parameter type. + */ + fun onUpDownButtonClicked(type: LaserParameter, state: UpDownState) { + when(type) { + LaserParameter.PulseWidth -> onClickPulseDuration(state) + LaserParameter.Fluence -> onClickFluence(state) + LaserParameter.Repetition -> onClickRepetition(state) + } + } + + fun onChangePulseDuration(angle: Float) { + val newPulseStep = angle.degreeToStep(totalSteps = PulseDurations.size) + setPulseAngle(newPulseStep.stepToDegree(totalSteps = PulseDurations.size)) + + val newPulseDuration = PulseDurations[newPulseStep] + // When pulse duration changes via slider, we use the first available fluence for the new list. + val firstFluence = energyTable.value.getKey2ListForKey1(newPulseDuration).firstOrNull() ?: 0f + + // Call the centralized helper, resetting the fluence slider. + updateLaserParameters( + newPulseDuration = newPulseDuration, + newFluence = firstFluence, + ) + } + + fun onClickPulseDuration(state: UpDownState) { + val currentStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size) + val newStep = if (state == UpDownState.Up) currentStep + 1 else currentStep - 1 + + if (newStep in PulseDurations.indices) { + setPulseAngle(newStep.stepToDegree(totalSteps = PulseDurations.size)) + + val newPulseDuration = PulseDurations[newStep] + val firstFluence = energyTable.value.getKey2ListForKey1(newPulseDuration).firstOrNull() ?: 0f + + // Call the centralized helper, resetting the fluence slider. + updateLaserParameters( + newPulseDuration = newPulseDuration, + newFluence = firstFluence, + ) + } + } + + fun onChangeFluence(angle: Float) { + setFluenceAngle(angle) + + val pulseStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size) + val currentPulseDuration = PulseDurations[pulseStep] + + val fluenceStep = angle.degreeToStep(totalSteps = fluenceList.value.size) + if (fluenceStep in fluenceList.value.indices) { + val newFluence = fluenceList.value[fluenceStep] + // Call the helper without resetting the fluence angle, as it's the one being changed. + updateLaserParameters( + newPulseDuration = currentPulseDuration, + newFluence = newFluence, + ) + } + } + + fun onClickFluence(state: UpDownState) { + val currentStep = fluenceAngle.value.degreeToStep(totalSteps = fluenceList.value.size) + val newStep = if (state == UpDownState.Up) currentStep + 1 else currentStep - 1 + + if (newStep in fluenceList.value.indices) { + setFluenceAngle(newStep.stepToDegree(totalSteps = fluenceList.value.size)) + + val pulseStep = pulseAngle.value.degreeToStep(totalSteps = PulseDurations.size) + val currentPulseDuration = PulseDurations[pulseStep] + val newFluence = fluenceList.value[newStep] + + // Call the helper without resetting the fluence angle. + updateLaserParameters( + newPulseDuration = currentPulseDuration, + newFluence = newFluence + ) + } + } + + fun onChangeRepetition(angle: Float) { + // This logic is simple and has no complex dependencies, so it can remain as is. + if (angle != repetitionAngle.value) { + setRepetitionAngle(angle) + } + setSelectedPresetIndex(0) + } + + fun onClickRepetition(state: UpDownState) { + // This logic is also simple and correct. + val currentStep = repetitionAngle.value.degreeToStep(totalSteps = repetitionList.value.size) + val newStep = if (state == UpDownState.Up) currentStep + 1 else currentStep - 1 + + if (newStep in repetitionList.value.indices) { + val newRepetitionAngle = newStep.stepToDegree(totalSteps = repetitionList.value.size) + setRepetitionAngle(newRepetitionAngle) + setSelectedPresetIndex(0) + } + } + + /** + * Perform all heavy I/O in a single background block. + * This prevents the "Skipped frames" caused by 30+ sequential bridge calls + * from MainActivity to MainViewModel. + */ + suspend fun performFullInitialization() { + withContext(dispatcherProvider.io) { + try { + // 1. Database tasks (Heavy) + trimSerialLog() + loadTemperatureDataFromDatabase() + + // 2. Load all preferences in parallel to save time + // This reduces the total time the background thread is "busy" + val tasks = listOf( + launch { loadHpCountFromPreference() }, + launch { loadLampCountFromPreference() }, + launch { loadDcdCountFromPreference() }, + launch { initSliderVolumeFromPreference() }, + launch { loadVoltageTableFromPreference() }, + launch { loadSprayDcdIndexFromPreference() }, + launch { loadPresetListFromPreference() }, + launch { loadSprayDcdFromPreference() }, + launch { loadSprayDcdListFromPreference() }, + launch { loadGuideBeamFromPreference() }, + launch { loadGuideBeamMaxFromPreference() }, + launch { loadGuideBeamMinFromPreference() }, + launch { loadOpTimeHourFromPreference() }, + launch { loadQSwitchFromPreference() }, + launch { loadLifeTimeFromPreference() }, + launch { loadTemeratureFromPreference() }, + launch { loadTemeratureWriteFromPreference() }, + launch { loadEnergyMeasuredWriteFromPreference() }, + launch { loadEnergyRefer2FromPreference() }, + launch { loadProductSerialListFromPreference() }, + launch { loadPowerSupplySerialListFromPreference() }, + launch { loadLaserHandSerialListFromPreference() } + ) + tasks.joinAll() // Wait for all to finish + + Timber.d("Initialization complete. UI switched.") + } catch (e: Exception) { + Timber.e(e, "performFullInitialization failed") + } + } + } + + init { + Timber.d("MainViewModel instance created") + // ONLY do light, non-IO variable initialization here. + // DO NOT call load... functions here. + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/splash/AnimcatedProgressBar.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/splash/AnimcatedProgressBar.kt new file mode 100644 index 0000000..c40d0d4 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/splash/AnimcatedProgressBar.kt @@ -0,0 +1,89 @@ +package com.laseroptek.raman.ui.screens.splash + + +import android.content.res.Configuration +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.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.LinearProgressIndicator +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R +import com.laseroptek.raman.const.preHeatingMillis +import com.laseroptek.raman.ui.theme.RobotoTypography +import com.laseroptek.raman.utils.ext.px +import kotlinx.coroutines.delay + +@Composable +fun AnimatedProgressBar( + onFinish: () -> Unit = {}, + durationMillis: Int = preHeatingMillis.toInt() +) { + var targetProgress by remember { mutableStateOf(0.1f) } // Target progress value + + val animatedProgress by animateFloatAsState( + targetValue = targetProgress, + animationSpec = tween(durationMillis = 1000), // Animation duration + label = "progressAnimation" + ) + + Column(modifier = Modifier + .size(width = 150.px.dp, height = 33.px.dp) + , horizontalAlignment = Alignment.CenterHorizontally + , verticalArrangement = Arrangement.Center) { + //Text(text = "Progress: ${(animatedProgress * 100).toInt()}%") + Text( + text = stringResource(R.string.loading), + style = RobotoTypography.bodySmall, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = Color(222,173,11), + textAlign = TextAlign.Center + ) + + Spacer(Modifier.weight(1f)) + + LinearProgressIndicator( + progress = { animatedProgress }, + color = Color(222,173,11), + trackColor= Color(222,173,11).copy(alpha = 0.3f), + ) + } + + // Simulate progress updates + LaunchedEffect(key1 = true) { + while (targetProgress < durationMillis) { + delay(100) // Update every 0.1 second + targetProgress += 100f + } + + onFinish.invoke() + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun AnimatedProgressBarPreview() { + AnimatedProgressBar() +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/splash/ShimmeringText.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/splash/ShimmeringText.kt new file mode 100644 index 0000000..7f21c07 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/splash/ShimmeringText.kt @@ -0,0 +1,61 @@ +package com.laseroptek.raman.ui.screens.splash + +import androidx.compose.animation.core.DurationBasedAnimationSpec +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import com.laseroptek.raman.ui.theme.RobotoTypography + +@Composable +fun ShimmeringText( + text: String, + shimmerColor: Color = Color(220, 165, 12), + textStyle: TextStyle = RobotoTypography.headlineSmall.copy( + color = Color(189, 115, 25), + fontWeight = FontWeight.Bold + ), + animationSpec: DurationBasedAnimationSpec = tween(1000, 500, LinearEasing) +) { + val infiniteTransition = rememberInfiniteTransition(label = "ShimmeringTextTransition") + + val shimmerProgress by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable(animationSpec), + label = "ShimmerProgress" + ) + + val brush = remember(shimmerProgress) { + object : ShaderBrush() { + override fun createShader(size: Size): Shader { + val initialXOffset = -size.width + val totalSweepDistance = size.width * 2 + val currentPosition = initialXOffset + totalSweepDistance * shimmerProgress + return LinearGradientShader( + colors = listOf(Color.Transparent, shimmerColor, Color.Transparent), + from = Offset(currentPosition, 0f), + to = Offset(currentPosition + size.width, 0f) + ) + } + } + } + + Text( + text = text, + style = textStyle.copy(brush = brush), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/splash/SplashView.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/splash/SplashView.kt new file mode 100644 index 0000000..ec77dfb --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/splash/SplashView.kt @@ -0,0 +1,65 @@ +package com.laseroptek.raman.ui.screens.splash + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.laseroptek.raman.R +import com.laseroptek.raman.const.preHeatingMillis +import com.laseroptek.raman.navigation.Routes +import com.laseroptek.raman.ui.screens.main.MainViewModel +import com.laseroptek.raman.utils.ext.px + + +@Composable +fun SplashView( + mainNavController: NavHostController = rememberNavController(), + mainViewModel: MainViewModel = hiltViewModel(), +) { + // 바탕 화면 + Box( + modifier = Modifier + .fillMaxSize(), + // .background(color = Color.Black), + contentAlignment = Alignment.Center + ) { + // Full Background Image + Image( + painter = painterResource(id = R.drawable.splash_background), + contentDescription = "Background Image", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + + Column( + modifier = Modifier.size(width = 200.px.dp, height = 97.px.dp).background(Color.Transparent), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + ShimmeringText(text = "LASEROPTEK",) + + Spacer(Modifier.weight(1f)) + + AnimatedProgressBar( + onFinish = { + mainNavController.navigate(Routes.Main.route) + }, + durationMillis = preHeatingMillis.toInt() + ) + } + } +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/theme/Color.kt b/app/src/main/java/com/laseroptek/raman/ui/theme/Color.kt new file mode 100644 index 0000000..80e0618 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/theme/Color.kt @@ -0,0 +1,59 @@ +package com.laseroptek.raman.ui.theme + +import androidx.compose.ui.graphics.Color + +val md_theme_light_primary = Color(0xFFBF0031) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFFFDAD9) +val md_theme_light_onPrimaryContainer = Color(0xFF40000A) +val md_theme_light_secondary = Color(0xFF775656) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFFFDAD9) +val md_theme_light_onSecondaryContainer = Color(0xFF2C1516) +val md_theme_light_tertiary = Color(0xFF755A2F) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFFFDDAF) +val md_theme_light_onTertiaryContainer = Color(0xFF281800) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFFFBFF) +val md_theme_light_onBackground = Color(0xFF201A1A) +val md_theme_light_surface = Color(0xFFFFFBFF) +val md_theme_light_onSurface = Color(0xFF201A1A) +val md_theme_light_surfaceVariant = Color(0xFFF4DDDD) +val md_theme_light_onSurfaceVariant = Color(0xFF524343) +val md_theme_light_outline = Color(0xFF857373) +val md_theme_light_inverseOnSurface = Color(0xFFFBEEED) +val md_theme_light_inverseSurface = Color(0xFF362F2F) +val md_theme_light_inversePrimary = Color(0xFFFFB3B4) +val md_theme_light_surfaceTint = Color(0xFFBF0031) + +val md_theme_dark_primary = Color(0xFFFFB3B4) +val md_theme_dark_onPrimary = Color(0xFF680016) +val md_theme_dark_primaryContainer = Color(0xFF920023) +val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD9) +val md_theme_dark_secondary = Color(0xFFE6BDBC) +val md_theme_dark_onSecondary = Color(0xFF44292A) +val md_theme_dark_secondaryContainer = Color(0xFF5D3F3F) +val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD9) +val md_theme_dark_tertiary = Color(0xFFE5C18D) +val md_theme_dark_onTertiary = Color(0xFF422C05) +val md_theme_dark_tertiaryContainer = Color(0xFF5B421A) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDAF) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF201A1A) +val md_theme_dark_onBackground = Color(0xFFECE0DF) +val md_theme_dark_surface = Color(0xFF201A1A) +val md_theme_dark_onSurface = Color(0xFFECE0DF) +val md_theme_dark_surfaceVariant = Color(0xFF524343) +val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C1) +val md_theme_dark_outline = Color(0xFFA08C8C) +val md_theme_dark_inverseOnSurface = Color(0xFF201A1A) +val md_theme_dark_inverseSurface = Color(0xFFECE0DF) +val md_theme_dark_inversePrimary = Color(0xFFBF0031) +val md_theme_dark_surfaceTint = Color(0xFFFFB3B4) diff --git a/app/src/main/java/com/laseroptek/raman/ui/theme/Shape.kt b/app/src/main/java/com/laseroptek/raman/ui/theme/Shape.kt new file mode 100644 index 0000000..ca779ff --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val ramanShapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(8.dp) +) diff --git a/app/src/main/java/com/laseroptek/raman/ui/theme/Theme.kt b/app/src/main/java/com/laseroptek/raman/ui/theme/Theme.kt new file mode 100644 index 0000000..d545a65 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/theme/Theme.kt @@ -0,0 +1,91 @@ +package com.laseroptek.raman.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, +) + +val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, +) + +@Composable +fun MainTheme( + //darkTheme: Boolean = isSystemInDarkTheme(), + colorScheme: androidx.compose.material3.ColorScheme = LightColors, + content: @Composable () -> Unit +) { + LocalContext.current + /* + val colorScheme = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } else { + if (darkTheme) DarkColors else LightColors + } + */ + + MaterialTheme( + colorScheme = colorScheme, + shapes = ramanShapes, + typography = MontserratTypography, + content = content, + ) +} diff --git a/app/src/main/java/com/laseroptek/raman/ui/theme/Type.kt b/app/src/main/java/com/laseroptek/raman/ui/theme/Type.kt new file mode 100644 index 0000000..73e1a86 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/ui/theme/Type.kt @@ -0,0 +1,447 @@ +package com.laseroptek.raman.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.unit.sp +import com.laseroptek.raman.R + +private val Montserrat = FontFamily( + Font(R.font.montserrat_regular), + Font(R.font.montserrat_medium, FontWeight.W500), +) + +private val RacingSansOne = FontFamily( + Font(R.font.racingsansone_regular), +) + +private val Pretendard = FontFamily( + Font(R.font.pretendard_black), + Font(R.font.pretendard_bold), + Font(R.font.pretendard_extrabold), + Font(R.font.pretendard_extralight), + Font(R.font.pretendard_light), + Font(R.font.pretendard_medium), + Font(R.font.pretendard_regular), + Font(R.font.pretendard_semibold), + Font(R.font.pretendard_thin), +) + +val roboto @Composable +get() = Roboto +private val Roboto = FontFamily( + Font(R.font.roboto_black), + Font(R.font.roboto_blackitalic), + Font(R.font.roboto_bold), + Font(R.font.roboto_bolditalic), + Font(R.font.roboto_italic), + Font(R.font.roboto_light), + Font(R.font.roboto_lightitalic), + Font(R.font.roboto_medium), + Font(R.font.roboto_mediumitalic), + Font(R.font.roboto_regular), + Font(R.font.roboto_thin), + Font(R.font.roboto_thinitalic) +) + +val robotoMono @Composable +get() = RobotoMono +private val RobotoMono = FontFamily( + Font(R.font.roboto_mono_bold), + Font(R.font.roboto_mono_bold_italic), + Font(R.font.roboto_mono_italic), + Font(R.font.roboto_mono_light_italic), + Font(R.font.roboto_mono_medium), + Font(R.font.roboto_mono_medium_italic), + Font(R.font.roboto_mono_regular), + Font(R.font.roboto_mono_thin), + Font(R.font.roboto_mono_thin_italic), + + ) + +@Suppress("DEPRECATION") +val defaultTextStyle = TextStyle( + fontFamily = Montserrat, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) +) + +val racingSansOneTextStyle = TextStyle( + fontFamily = RacingSansOne, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) +) + +val pretendardTextStyle = TextStyle( + fontFamily = Pretendard, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) +) + +val robotoTextStyle = TextStyle( + fontFamily = Roboto, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) +) + +val robotoMonoTextStyle = TextStyle( + fontFamily = RobotoMono, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) +) + +val RacingSansOneTypography = Typography( + displayLarge = racingSansOneTextStyle.copy( + fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp + ), + displayMedium = racingSansOneTextStyle.copy( + fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp + ), + displaySmall = racingSansOneTextStyle.copy( + fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp + ), + headlineLarge = racingSansOneTextStyle.copy( + fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineMedium = racingSansOneTextStyle.copy( + fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineSmall = racingSansOneTextStyle.copy( + fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleLarge = racingSansOneTextStyle.copy( + fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleMedium = racingSansOneTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + titleSmall = racingSansOneTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + labelLarge = racingSansOneTextStyle.copy( + fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, fontWeight = FontWeight.Medium + ), + labelMedium = racingSansOneTextStyle.copy( + fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + labelSmall = racingSansOneTextStyle.copy( + fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + bodyLarge = racingSansOneTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineBreak = LineBreak.Paragraph + ), + bodyMedium = racingSansOneTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + lineBreak = LineBreak.Paragraph + ), + bodySmall = racingSansOneTextStyle.copy( + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + lineBreak = LineBreak.Paragraph + ), +) + +val PretendardTypography = Typography( + displayLarge = pretendardTextStyle.copy( + fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp + ), + displayMedium = pretendardTextStyle.copy( + fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp + ), + displaySmall = pretendardTextStyle.copy( + fontSize = 30.sp, lineHeight = 44.sp, letterSpacing = 0.sp + ), + headlineLarge = pretendardTextStyle.copy( + fontSize = 28.sp, lineHeight = 40.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineMedium = pretendardTextStyle.copy( + fontSize = 26.sp, lineHeight = 36.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineSmall = pretendardTextStyle.copy( + fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleLarge = pretendardTextStyle.copy( + fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleMedium = pretendardTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + titleSmall = pretendardTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + labelLarge = pretendardTextStyle.copy( + fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, fontWeight = FontWeight.Medium + ), + labelMedium = pretendardTextStyle.copy( + fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + labelSmall = pretendardTextStyle.copy( + fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + bodyLarge = pretendardTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineBreak = LineBreak.Paragraph + ), + bodyMedium = pretendardTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + lineBreak = LineBreak.Paragraph + ), + bodySmall = pretendardTextStyle.copy( + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + lineBreak = LineBreak.Paragraph + ), +) + +val RobotoTypography = Typography( + displayLarge = robotoTextStyle.copy( + fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp + ), + displayMedium = robotoTextStyle.copy( + fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp + ), + displaySmall = robotoTextStyle.copy( + fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp + ), + headlineLarge = robotoTextStyle.copy( + fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineMedium = robotoTextStyle.copy( + fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineSmall = robotoTextStyle.copy( + fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleLarge = robotoTextStyle.copy( + fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleMedium = robotoTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + titleSmall = robotoTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + labelLarge = robotoTextStyle.copy( + fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, fontWeight = FontWeight.Medium + ), + labelMedium = robotoTextStyle.copy( + fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + labelSmall = robotoTextStyle.copy( + fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + bodyLarge = robotoTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineBreak = LineBreak.Paragraph + ), + bodyMedium = robotoTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + lineBreak = LineBreak.Paragraph + ), + bodySmall = robotoTextStyle.copy( + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + lineBreak = LineBreak.Paragraph + ), +) + +val RobotoMonoTypography = Typography( + displayLarge = robotoMonoTextStyle.copy( + fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp + ), + displayMedium = robotoMonoTextStyle.copy( + fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp + ), + displaySmall = robotoMonoTextStyle.copy( + fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp + ), + headlineLarge = robotoMonoTextStyle.copy( + fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineMedium = robotoMonoTextStyle.copy( + fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineSmall = robotoMonoTextStyle.copy( + fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleLarge = robotoMonoTextStyle.copy( + fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleMedium = robotoMonoTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + titleSmall = robotoMonoTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + labelLarge = robotoMonoTextStyle.copy( + fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, fontWeight = FontWeight.Medium + ), + labelMedium = robotoMonoTextStyle.copy( + fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + labelSmall = robotoMonoTextStyle.copy( + fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + bodyLarge = robotoMonoTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineBreak = LineBreak.Paragraph + ), + bodyMedium = robotoMonoTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + lineBreak = LineBreak.Paragraph + ), + bodySmall = robotoMonoTextStyle.copy( + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + lineBreak = LineBreak.Paragraph + ), +) + +val MontserratTypography = Typography( + displayLarge = defaultTextStyle.copy( + fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp + ), + displayMedium = defaultTextStyle.copy( + fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp + ), + displaySmall = defaultTextStyle.copy( + fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp + ), + headlineLarge = defaultTextStyle.copy( + fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineMedium = defaultTextStyle.copy( + fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + headlineSmall = defaultTextStyle.copy( + fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleLarge = defaultTextStyle.copy( + fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp, lineBreak = LineBreak.Heading + ), + titleMedium = defaultTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + titleSmall = defaultTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + fontWeight = FontWeight.Medium, + lineBreak = LineBreak.Heading + ), + labelLarge = defaultTextStyle.copy( + fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, fontWeight = FontWeight.Medium + ), + labelMedium = defaultTextStyle.copy( + fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + labelSmall = defaultTextStyle.copy( + fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, fontWeight = FontWeight.Medium + ), + bodyLarge = defaultTextStyle.copy( + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineBreak = LineBreak.Paragraph + ), + bodyMedium = defaultTextStyle.copy( + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + lineBreak = LineBreak.Paragraph + ), + bodySmall = defaultTextStyle.copy( + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + lineBreak = LineBreak.Paragraph + ), +) diff --git a/app/src/main/java/com/laseroptek/raman/utils/BottomShadow.kt b/app/src/main/java/com/laseroptek/raman/utils/BottomShadow.kt new file mode 100644 index 0000000..ba8ae88 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/BottomShadow.kt @@ -0,0 +1,27 @@ +package com.laseroptek.raman.utils + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun BottomShadow(alpha: Float = 0.1f, height: Dp = 8.dp) { + Box(modifier = Modifier.fillMaxWidth() + .height(height) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Black.copy(alpha = alpha), + Color.Transparent, + ) + ) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/DispatcherProvider.kt b/app/src/main/java/com/laseroptek/raman/utils/DispatcherProvider.kt new file mode 100644 index 0000000..2a4c947 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/DispatcherProvider.kt @@ -0,0 +1,27 @@ +package com.laseroptek.raman.utils + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +interface DispatcherProvider { + + val main: CoroutineDispatcher + + val io: CoroutineDispatcher + + val default: CoroutineDispatcher + +} + +class DefaultDispatcherProvider : DispatcherProvider { + + override val main: CoroutineDispatcher + get() = Dispatchers.Main + + override val io: CoroutineDispatcher + get() = Dispatchers.IO + + override val default: CoroutineDispatcher + get() = Dispatchers.Default + +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/Double.kt b/app/src/main/java/com/laseroptek/raman/utils/Double.kt new file mode 100644 index 0000000..24c6831 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/Double.kt @@ -0,0 +1,28 @@ +package com.laseroptek.raman.utils + +import com.laseroptek.raman.data.model.chart.IndicatorCount + + +fun split( + count: IndicatorCount, + minValue: Double, + maxValue: Double, +):List{ + return when (count) { + is IndicatorCount.CountBased -> { + val step = (maxValue - minValue) / (count.count - 1) + val result = (0 until count.count).map { (maxValue - it * step) } + result + } + + is IndicatorCount.StepBased -> { + val result = mutableListOf() + var cache = maxValue + while (cache > minValue) { + result.add(cache.coerceAtLeast(minValue)) + cache -= count.stepBy + } + result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ErrorMessage.kt b/app/src/main/java/com/laseroptek/raman/utils/ErrorMessage.kt new file mode 100644 index 0000000..c2596a7 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ErrorMessage.kt @@ -0,0 +1,5 @@ +package com.laseroptek.raman.utils + +import androidx.annotation.StringRes + +data class ErrorMessage(val id: Long, @param:StringRes val messageId: Int) diff --git a/app/src/main/java/com/laseroptek/raman/utils/FixedSizeFIFOQueue.kt b/app/src/main/java/com/laseroptek/raman/utils/FixedSizeFIFOQueue.kt new file mode 100644 index 0000000..a9bb8ea --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/FixedSizeFIFOQueue.kt @@ -0,0 +1,42 @@ +package com.laseroptek.raman.utils + +import java.util.LinkedList + + +class FixedSizeFIFOQueue(private val maxSize: Int) { + + private val queue = LinkedList() + + val size: Int + get() = queue.size + + fun add(element: E) { + if (queue.size >= maxSize) { + // Remove the OLDEST item (from the front) + queue.pollFirst() // was pollLast() + } + // Add the NEWEST item to the end. + queue.addLast(element) // was addFirst() + } + + fun addAll(elements: Collection) { + elements.forEach { element -> + if (queue.size >= maxSize) { + queue.pollFirst() // was pollLast() + } + queue.addLast(element) // was addFirst() + } + } + + fun toList(): List { + return queue.toList() + } + + fun isEmpty(): Boolean { + return !queue.isNotEmpty() + } + + fun isNotEmpty(): Boolean { + return queue.isNotEmpty() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/FixedSizeLIFOQueue.kt b/app/src/main/java/com/laseroptek/raman/utils/FixedSizeLIFOQueue.kt new file mode 100644 index 0000000..0335b60 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/FixedSizeLIFOQueue.kt @@ -0,0 +1,67 @@ +package com.laseroptek.raman.utils + + +/* +val queue = FixedSizeLIFOQueue(3) +queue.add(1) +queue.add(2) +queue.add(3) +queue.add(4) // Drops the oldest element (1) + +val elements = queue.toList() +println(elements) // Output: [2, 3, 4] + +val itemWithValue = queue.queue.firstOrNull { it.property == targetValue } + */ +class FixedSizeLIFOQueue(private val maxSize: Int) { + + private val queue = ArrayDeque(maxSize) + + val size: Int + get() = queue.size + + fun add(element: T) { + if (queue.size == maxSize) { + queue.removeFirst() // Remove the oldest element + } + queue.addLast(element) // Add the new element + } + + fun addAll(elements: Collection) { + if (maxSize <= 0 || elements.isEmpty()) { + return // Nothing to add or no space + } + + elements.forEach { element -> + add( element ) + } + } + + fun remove(): T? { + return queue.removeLastOrNull() // Remove and return the last element + } + + fun clear() { + queue.clear() + } + + fun peek(): T? { + return queue.lastOrNull() // Return the last element without removing it + } + + fun isEmpty(): Boolean { + return queue.isEmpty() + } + + fun isNotEmpty(): Boolean { + return queue.isNotEmpty() + } + + fun isFull(): Boolean { + return queue.size == maxSize + } + + fun toList(): List { + return queue.toList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/FlexibleArray2D.kt b/app/src/main/java/com/laseroptek/raman/utils/FlexibleArray2D.kt new file mode 100644 index 0000000..46ebc29 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/FlexibleArray2D.kt @@ -0,0 +1,79 @@ +package com.laseroptek.raman.utils + +data class FlexibleArray2D( + private var data: MutableMap, Float> = mutableMapOf() +) { + fun setData(newData: MutableMap, Float>) { + data = newData + } + + fun getData(): Map, Float> { + return data + } + + fun add(row: Float, col: Float, value: Float) { + data[Pair(row, col)] = value + } + + fun get(row: Float, col: Float): Float? { + return data[Pair(row, col)] + } + + fun isEmpty(): Boolean { + return data.isEmpty() + } + + fun FlexibleArray2D.getRowValues(row: Float): List { + return data.filterKeys { it.first == row }.values.toList() + } + + fun FlexibleArray2D.getColValues(col: Float): List { + return data.filterKeys { it.second == col }.values.toList() + } + + fun FlexibleArray2D.clone(): FlexibleArray2D { + val newData = data.toMutableMap() // Create a deep copy of the data map + return FlexibleArray2D(newData) + } + +} + +/* +val originalArray = FlexibleArray2D() +// ... initialize values ... +val clonedArray = originalArray.clone() +*/ + +/* +val array2D = FlexibleArray2D() // ... initialize values ... +val rowValues = array2D.getRowValues(1) // Get values of row 1 +*/ + +/* +val initialData = mapOf( + Pair(0f, 0f) to 1f, + Pair(0f, 1f) to 2f, + Pair(1f, 0f) to 3f, + Pair(1f, 1f) to 4f +) +val array2D = FlexibleArray2D(initialData) +*/ + +/* +val array2D = FlexibleArray2D() +array2D.add(0f, 0f, 1f) +array2D.add(0f, 1f, 2f) +array2D.add(1f, 0f, 3f) +array2D.add(1f, 1f, 4f) +*/ + +/* +val rows = 3 +val cols = 4 +val array2D = FlexibleArray2D() +for (i in 0 until rows) { + for (j in 0 until cols) { + array2D.add(i, j, i * cols + j + 1) // Example: initialize with sequential values + } +} +*/ \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/GradientButton.kt b/app/src/main/java/com/laseroptek/raman/utils/GradientButton.kt new file mode 100644 index 0000000..cb5fc74 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/GradientButton.kt @@ -0,0 +1,226 @@ +package com.laseroptek.raman.utils + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + + +@Composable +fun GradientButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + text: String, + width: Dp = 80.dp, + height: Dp = 42.dp, + cornerRadius: Dp = 52.dp, + + dropShadowColor: Color = Color(0,0,0).copy(alpha = 0.14f), + dropShadowOffsetX: Dp = 0.dp, + dropShadowOffsetY: Dp = 5.dp, + dropShadowBlurRadius: Dp = 7.dp, + dropShadowSpread: Dp = 0.dp, + + innerShadowColor: Color = Color(255,255,255).copy(alpha = 1f), + innerShadowOffsetX: Dp = 0.dp, + innerShadowOffsetY: Dp = 4.dp, + innerShadowBlurRadius: Dp = 1.dp, + innerShadowSpread: Dp = 0.dp, + + gradientStartColor: Color = Color(250,250,250).copy(alpha = 1f), + gradientEndColor: Color = Color(198,198,198).copy(alpha = 1f), + + borderGradientStartColor: Color = Color(81,81,81).copy(alpha = 1f), + borderGradientEndColor: Color = Color(64,64,64).copy(alpha = 1f), + borderWidth: Dp = 3.dp, + + textColor: Color = Color.Black, + iconId: Int? = null // Add an optional iconId parameter +) { + Button( + onClick = onClick, + modifier = modifier + .size(width, height) + .shadow( + elevation = dropShadowBlurRadius, + shape = RoundedCornerShape(cornerRadius), + ambientColor = dropShadowColor, + spotColor = dropShadowColor, + clip = false + ) + .graphicsLayer { + translationX = dropShadowOffsetX.toPx() + translationY = dropShadowOffsetY.toPx() + } + .drawBehind { + // Draw the background gradient + drawRoundRect( + brush = Brush.linearGradient( + colors = listOf(gradientStartColor, gradientEndColor) + ), + cornerRadius = CornerRadius(cornerRadius.toPx()) + ) + + // Draw the inner shadow + drawInnerShadow( + color = innerShadowColor, + offsetX = innerShadowOffsetX.toPx(), + offsetY = innerShadowOffsetY.toPx(), + blurRadius = innerShadowBlurRadius.toPx(), + spread = innerShadowSpread.toPx(), + cornerRadius = cornerRadius.toPx() + ) + + // Draw the gradient border + drawGradientBorder( + borderWidth = borderWidth.toPx(), + cornerRadius = cornerRadius.toPx(), + gradientStartColor = borderGradientStartColor, + gradientEndColor = borderGradientEndColor + ) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, // Make the button's background transparent + contentColor = textColor + ), + contentPadding = PaddingValues(0.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + // Display the icon if iconId is provided + if (iconId != null) { + Image( + painter = painterResource(id = iconId), + contentDescription = "Button Icon", + modifier = Modifier.size(24.dp) // Adjust the size as needed + ) + Spacer(modifier = Modifier.width(8.dp)) // Add space between icon and text + } + Text(text = text) + } + } +} + +fun DrawScope.drawInnerShadow( + color: Color, + offsetX: Float, + offsetY: Float, + blurRadius: Float, + spread: Float, + cornerRadius: Float +) { + val shadowPath = Path().apply { + addRoundRect( + RoundRect( + rect = size.toRect(), + cornerRadius = CornerRadius(cornerRadius) + ) + ) + } + + val shadowPaint = android.graphics.Paint().apply { + this.color = color.toArgb() + maskFilter = android.graphics.BlurMaskFilter(blurRadius, android.graphics.BlurMaskFilter.Blur.NORMAL) + } + + val shadowBitmap = android.graphics.Bitmap.createBitmap( + size.width.roundToInt(), + size.height.roundToInt(), + android.graphics.Bitmap.Config.ARGB_8888 + ) + + val shadowCanvas = android.graphics.Canvas(shadowBitmap) + shadowCanvas.drawPath(shadowPath.asAndroidPath(), shadowPaint) + + //val shadowOffset = Offset(offsetX, offsetY) + // Convert Offset to IntOffset + val shadowIntOffset = androidx.compose.ui.unit.IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) + + drawPath( + path = shadowPath, + color = color, + alpha = 1f, + blendMode = androidx.compose.ui.graphics.BlendMode.SrcIn + ) + drawPath( + path = shadowPath, + color = color, + alpha = 1f, + blendMode = androidx.compose.ui.graphics.BlendMode.SrcOut + ) + drawImage( + image = shadowBitmap.asImageBitmap(), + dstOffset = shadowIntOffset, + alpha = 1f, + blendMode = androidx.compose.ui.graphics.BlendMode.SrcAtop + ) +} + +fun DrawScope.drawGradientBorder( + borderWidth: Float, + cornerRadius: Float, + gradientStartColor: Color, + gradientEndColor: Color +) { + val path = Path().apply { + addRoundRect( + RoundRect( + rect = size.toRect(), + cornerRadius = CornerRadius(cornerRadius) + ) + ) + } + + drawPath( + path = path, + brush = Brush.linearGradient( + colors = listOf(gradientStartColor, gradientEndColor) + ), + style = Stroke(width = borderWidth) + ) +} + +@Preview(showBackground = true) +@Composable +fun GradientButtonPreview() { + Box(modifier = Modifier.fillMaxSize().background(Color.Gray)) { + GradientButton( + onClick = { /* Handle click */ }, + text = "Gradient", + modifier = Modifier.padding(16.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/Height.kt b/app/src/main/java/com/laseroptek/raman/utils/Height.kt new file mode 100644 index 0000000..08a3804 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/Height.kt @@ -0,0 +1,26 @@ +package com.laseroptek.raman.utils + +/** + * This function calculates offset from total for a specific value + */ +fun calculateOffset( + maxValue: Double, + minValue: Double, + total: Float, + value:Float +): Double { + val range = maxValue - minValue + val percentage = (value - minValue) / range + val offset = total * percentage + return offset +} + +/** + * This function is reverse of calculateOffset, calculates value from total value for a specific offset + */ +fun calculateValue(minValue: Double, maxValue: Double, total: Float, offset:Float): Double { + val percentage = offset / total + val range = maxValue - minValue + val value = minValue + percentage * range + return value +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ImportFileFromUsb.kt b/app/src/main/java/com/laseroptek/raman/utils/ImportFileFromUsb.kt new file mode 100644 index 0000000..a6993e8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ImportFileFromUsb.kt @@ -0,0 +1,168 @@ +package com.laseroptek.raman.utils + +import android.content.Context +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader + +// Helper function to read content from URI +suspend fun readFileFromUri( + context: Context, + uri: Uri +): String? { // Returns String content, or null on failure + return withContext(Dispatchers.IO) { + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + BufferedReader(InputStreamReader(inputStream)).use { reader -> + reader.readText() // Reads entire content as String + } + } ?: run { + Timber.e("Failed to open input stream for URI: $uri") + null + } + } catch (e: IOException) { + Timber.e(e, "Error reading file from $uri") + null + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImportFileFromUsbScreen( + modifier: Modifier = Modifier, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + coroutineScope: CoroutineScope = rememberCoroutineScope() +) { + val context = LocalContext.current + var isLoading by remember { mutableStateOf(false) } + var fileContentPreview by remember { mutableStateOf(null) } // To show a snippet of imported content + + val openFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), // Contract for opening an existing document + onResult = { uri: Uri? -> + uri?.let { selectedUri -> + isLoading = true + fileContentPreview = null // Clear previous preview + coroutineScope.launch { + val content = readFileFromUri(context, selectedUri) + isLoading = false + if (content != null) { + fileContentPreview = content.take(200) // Show first 200 chars as preview + snackbarHostState.showSnackbar("File imported successfully!", duration = SnackbarDuration.Short) + Timber.d("File content loaded from $selectedUri") + // TODO: Process the full 'content' as needed (e.g., parse JSON, update database, etc.) + } else { + snackbarHostState.showSnackbar("Failed to read file.", duration = SnackbarDuration.Short) + } + } + } ?: run { + Timber.d("Import cancelled by user.") + coroutineScope.launch { + snackbarHostState.showSnackbar("Import cancelled.", duration = SnackbarDuration.Short) + } + } + } + ) + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (isLoading) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(16.dp)) + Text("Importing...") + } else { + Button( + onClick = { + // Launch the file picker, filtering for text files. + // You can specify multiple MIME types: arrayOf("text/plain", "application/json") + openFileLauncher.launch(arrayOf("text/plain", "*/*")) // Example: text files or any file + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Import Text File from USB/External") + } + + fileContentPreview?.let { + Spacer(modifier = Modifier.height(24.dp)) + Text("File Preview (first 200 chars):", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + maxLines = 10, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 200.dp) // Limit preview height + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +fun ImportFileFromUsbScreenPreview() { + // Initialize Timber for Preview if not already done in an Application class + // Timber.plant(Timber.DebugTree()) + + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + MaterialTheme { + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { TopAppBar(title = { Text("Import File Demo") }) } + ) { paddingValues -> + ImportFileFromUsbScreen( + modifier = Modifier.padding(paddingValues), + snackbarHostState = snackbarHostState, + coroutineScope = coroutineScope + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/LocaleHelper.kt b/app/src/main/java/com/laseroptek/raman/utils/LocaleHelper.kt new file mode 100644 index 0000000..fdde0a1 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/LocaleHelper.kt @@ -0,0 +1,23 @@ +package com.laseroptek.raman.utils + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Configuration +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import java.util.Locale + +object LocaleHelper { + fun setLocale(context: Context, language: String): ContextWrapper { + val locale = Locale.forLanguageTag(language) + Locale.setDefault(locale) + + val configuration = Configuration(context.resources.configuration) + configuration.setLocale(locale) + + val localeList = LocaleListCompat.create(locale) + AppCompatDelegate.setApplicationLocales(localeList) + + return ContextWrapper(context.createConfigurationContext(configuration)) + } +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/PreferenceBackupManager.kt b/app/src/main/java/com/laseroptek/raman/utils/PreferenceBackupManager.kt new file mode 100644 index 0000000..e644e7f --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/PreferenceBackupManager.kt @@ -0,0 +1,158 @@ +package com.laseroptek.raman.utils + + +import android.content.Context +import android.net.Uri +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull +import timber.log.Timber +import java.io.InputStreamReader +import java.io.OutputStreamWriter + +class PreferenceBackupManager( + private val context: Context, + private val dataStore: DataStore +) { + // A single, pretty-printing Gson instance is all we need. + private val prettyGson: Gson = GsonBuilder() + .setPrettyPrinting() + .create() + + // ----- EXPORT TO JSON ----- + suspend fun exportPreferencesToUri(uri: Uri): Boolean { + return try { + // 1. Get the map of all preferences. Values will be native types OR JSON strings. + val allPrefsMap: Map = dataStore.data + .mapNotNull { preferences -> + preferences.asMap().mapKeys { it.key.name } + } + .first() + + if (allPrefsMap.isEmpty()) { + Timber.d("No preferences to export.") + return true + } + + // 2. Iterate through the raw map and build a new map with the correct types. + val processedMap = mutableMapOf() + for ((key, value) in allPrefsMap) { + if (value is String && (value.trim().startsWith("{") || value.trim().startsWith("["))) { + // If the value is a String that looks like JSON, parse it. + try { + // This converts the JSON string into a structured JsonElement (JsonObject or JsonArray) + processedMap[key] = JsonParser.parseString(value) + } catch (e: Exception) { + // If parsing fails, just keep it as a raw string. + processedMap[key] = value + } + } else { + // For native types (Int, Float, Boolean, etc.), add them directly. + processedMap[key] = value + } + } + + // 3. Serialize the new, clean map to a pretty-printed JSON string. + // Gson will correctly handle the JsonElements and native types. + val jsonString = prettyGson.toJson(processedMap) + + // 4. Write the JSON string to the file. + context.contentResolver.openOutputStream(uri)?.use { outputStream -> + OutputStreamWriter(outputStream, "UTF-8").use { writer -> + writer.write(jsonString) + } + } ?: run { + Timber.e("Failed to open output stream for URI: $uri") + return false + } + + Timber.d("Preferences exported successfully to $uri as JSON") + true + } catch (e: Exception) { + Timber.e(e, "Error exporting preferences to JSON") + false + } + } + + // ----- IMPORT FROM JSON ----- + suspend fun importPreferencesFromUri(uri: Uri, clearExisting: Boolean = true): Boolean { + return try { + // 1. Read the JSON string from the file. + val jsonString = context.contentResolver.openInputStream(uri)?.use { + InputStreamReader(it, "UTF-8").readText() + } ?: run { + Timber.e("Failed to open input stream for URI: $uri") + return false + } + + if (jsonString.isBlank()) { + Timber.w("Import file is empty.") + return true + } + + // 2. Deserialize the JSON into a Map. Gson handles this perfectly. + val type = object : TypeToken>() {}.type + val importedPrefsMap: Map = prettyGson.fromJson(jsonString, type) + + // 3. Write the imported values back to DataStore. + dataStore.edit { preferences -> + if (clearExisting) { + preferences.clear() + } + for ((keyName, value) in importedPrefsMap) { + assignPreferenceValue(preferences, keyName, value) + } + } + Timber.d("Preferences imported successfully from JSON") + true + } catch (e: Exception) { + Timber.e(e, "Error importing preferences from JSON") + false + } + } + + /** + * Helper to write values back to DataStore, respecting their original types. + */ + private fun assignPreferenceValue(prefs: MutablePreferences, keyName: String, value: Any?) { + when (value) { + is String -> { + // If the value is a String, it might be a plain string OR a JSON representation of a complex object. + // The `stringPreferencesKey` handles both cases correctly. + prefs[stringPreferencesKey(keyName)] = value + } + is Boolean -> prefs[booleanPreferencesKey(keyName)] = value + is Double -> { // Gson deserializes all numbers as Double. + // If it's a whole number, decide if it was an Int or Long. + if (value == value.toLong().toDouble()) { + if (value <= Int.MAX_VALUE && value >= Int.MIN_VALUE) { + prefs[intPreferencesKey(keyName)] = value.toInt() + } else { + prefs[longPreferencesKey(keyName)] = value.toLong() + } + } else { // It's a floating-point number. + prefs[floatPreferencesKey(keyName)] = value.toFloat() + } + } + // `assignPreferenceValue` does not need to handle lists, as Gson deserializes them into LinkedTreeMap + // which can be converted back to a JSON string if needed, but our current save logic just needs the string. + else -> { + // For complex objects deserialized by Gson (like lists, which become LinkedTreeMap), + // we re-serialize them back to a JSON string to store in DataStore. + prefs[stringPreferencesKey(keyName)] = prettyGson.toJson(value) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/SharedPreferenceHelper.kt b/app/src/main/java/com/laseroptek/raman/utils/SharedPreferenceHelper.kt new file mode 100644 index 0000000..ce722a6 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/SharedPreferenceHelper.kt @@ -0,0 +1,36 @@ +package com.laseroptek.raman.utils + +import android.content.Context +import android.content.SharedPreferences + +object SharedPreferenceHelper { + // Define a name for your preference file + private const val PREFS_FILENAME = "com.laseroptek.raman.prefs" + + /** + * Saves a string value to SharedPreferences. + * + * @param context The application context. + * @param key The name of the preference to modify. + * @param value The new value for the preference. + */ + fun saveString(context: Context, key: String, value: String) { + val sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE) + val editor: SharedPreferences.Editor = sharedPreferences.edit() + editor.putString(key, value) + editor.apply() // Asynchronously saves the changes. Use commit() for synchronous save. + } + + /** + * Retrieves a string value from SharedPreferences. + * + * @param context The application context. + * @param key The name of the preference to retrieve. + * @param defaultValue The value to return if this preference does not exist. + * @return The preference value if it exists, or defaultValue. + */ + fun getString(context: Context, key: String, defaultValue: String? = null): String? { + val sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE) + return sharedPreferences.getString(key, defaultValue) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/SystemSound.kt b/app/src/main/java/com/laseroptek/raman/utils/SystemSound.kt new file mode 100644 index 0000000..d43a9e6 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/SystemSound.kt @@ -0,0 +1,89 @@ +package com.laseroptek.raman.utils + +import android.content.Context +import android.media.AudioManager + +object SystemSound { + fun getCurrentSystemVolume(context: Context, streamType: Int): Int { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + return audioManager.getStreamVolume(streamType) + } + + fun setCurrentSystemVolume(context: Context, streamType: Int, level: Int) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + return audioManager.setStreamVolume(streamType, level, AudioManager.FLAG_PLAY_SOUND) + } + + fun getMaximumVolumeForStream(context: Context, streamType: Int): Int { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + return audioManager.getStreamMaxVolume(streamType) + } + + fun getMinimumVolumeForStream(context: Context, streamType: Int): Int { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + audioManager.getStreamMinVolume(streamType) + } else { + 0 // Common practice for older APIs + } + } +} + +/* +@Composable +fun PlayBeepSound() { + val toneGenerator = remember { ToneGenerator(AudioManager.STREAM_ALARM, 100) } + + DisposableEffect(Unit) { + toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP, 150 * 10) + onDispose { + toneGenerator.release() // Release the ToneGenerator when the composable is disposed + } + } +} + +@Composable +fun PlayAlertSound(type: Int) { + val context = LocalContext.current + + LaunchedEffect(Unit) { + //val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + //val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) + //val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + val notification = RingtoneManager.getDefaultUri(type) + val r = RingtoneManager.getRingtone(context, notification) + r.play() + } +} + +@Composable +fun PlayDefaultSystemAlertSound(isPlaying: Boolean) { + val context = LocalContext.current + val notificationUri = remember { RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) } + val ringtone = remember { RingtoneManager.getRingtone(context, notificationUri) } + + DisposableEffect(isPlaying) { + if (isPlaying) { + ringtone.play() + } else { + ringtone.stop() + } + onDispose { + ringtone.stop() // Ensure sound is stopped when composable is disposed + } + } +} +*/ + +/* +@Composable +fun MyScreen() { + var isSoundPlaying by remember { mutableStateOf(false) } + + PlayDefaultSystemAlertSound(isPlaying = isSoundPlaying) + + Button(onClick = { isSoundPlaying = !isSoundPlaying }) { + Text(if (isSoundPlaying) "Stop Sound" else "Play Sound") + } +} +*/ \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/chart/Gradient.kt b/app/src/main/java/com/laseroptek/raman/utils/chart/Gradient.kt new file mode 100644 index 0000000..ff42290 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/chart/Gradient.kt @@ -0,0 +1,39 @@ +package ir.ehsannarmani.compose_charts.extensions.line_chart + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas + +internal fun DrawScope.drawLineGradient( + path: Path, + color1: Color, + color2: Color, + progress: Float, + size: Size? = null +) { + val _size = size ?: this.size + drawIntoCanvas { + val p = Path() + p.addPath(path) + p.lineTo(_size.width, _size.height) + p.lineTo(0f, _size.height) + p.close() + val paint = Paint() + paint.shader = LinearGradientShader( + Offset(0f, 0f), + Offset(0f, _size.height), + listOf( + color1.copy(alpha = color1.alpha * progress), + color2, + ), + tileMode = TileMode.Mirror + ) + it.drawPath(p, paint) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/chart/Line.kt b/app/src/main/java/com/laseroptek/raman/utils/chart/Line.kt new file mode 100644 index 0000000..dbd0dac --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/chart/Line.kt @@ -0,0 +1,52 @@ +package ir.ehsannarmani.compose_charts.extensions.line_chart + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.laseroptek.raman.utils.calculateOffset + +internal fun DrawScope.getLinePath( + dataPoints: List, + maxValue: Float, + minValue: Float, + rounded: Boolean = true, + size: Size? = null +): Path { + var processedDataPoints = dataPoints // Start with the original list + + if (dataPoints.size == 1) { + processedDataPoints = dataPoints + dataPoints.first() + } + + val _size = size ?: this.size + val path = Path() + if (processedDataPoints.isEmpty()) return path + val calculateHeight = { value: Float -> + calculateOffset( + maxValue = maxValue.toDouble(), + minValue = minValue.toDouble(), + total = _size.height, + value = value + ) + } + + path.moveTo(0f, (_size.height - calculateHeight(processedDataPoints[0])).toFloat()) + + for (i in 0 until processedDataPoints.size -1) { + val x1 = (i * (_size.width / (processedDataPoints.size - 1))) + val y1 = _size.height - calculateHeight(processedDataPoints[i]).toFloat() + val x2 = ((i + 1) * (_size.width / (processedDataPoints.size - 1))) + val y2 = _size.height - calculateHeight(processedDataPoints[i + 1]).toFloat() + + if (rounded) { + val cx = (x1 + x2) / 2f + path.cubicTo(x1 = cx, y1 = y1, x2 = cx, y2 = y2, x3 = x2, y3 = y2) + } else { + path.cubicTo(x1, y1, x1, y1, (x1 + x2) / 2, (y1 + y2) / 2) + path.cubicTo((x1 + x2) / 2, (y1 + y2) / 2, x2, y2, x2, y2) + + } + } + + return path +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/chart/PopupHelper.kt b/app/src/main/java/com/laseroptek/raman/utils/chart/PopupHelper.kt new file mode 100644 index 0000000..841a5ae --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/chart/PopupHelper.kt @@ -0,0 +1,90 @@ +package ir.ehsannarmani.compose_charts.extensions.line_chart + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import com.laseroptek.raman.utils.calculateOffset +import com.laseroptek.raman.utils.calculateValue +import kotlin.math.floor +import kotlin.math.pow + +internal data class Value( + val calculatedValue: Double, + val offset: Offset, +) + +internal fun getPopupValue( + points: List, + fraction: Double, + rounded: Boolean = false, + size: Size, + minValue: Double, + maxValue: Double +): Value { + val index = fraction * (points.count() - 1) + val roundedIndex = floor(index).toInt() + return if (fraction == 1.0) { + val lastPoint = points.last() + val offset = Offset( + x = size.width, + y = size.height - calculateOffset( + minValue = minValue, + maxValue = maxValue, + total = size.height, + value = lastPoint.toFloat() + ).toFloat() + ) + Value(calculatedValue = points.last(), offset = offset) + } else { + if (rounded && points.count() > 1) { + val calculateHeight = { value: Double -> + calculateOffset( + maxValue = maxValue, + minValue = minValue, + total = size.height, + value = value.toFloat() + ) + } + val x1 = (roundedIndex * (size.width / (points.size - 1))) + val x2 = ((roundedIndex + 1) * (size.width / (points.size - 1))) + val y1 = size.height - calculateHeight(points[roundedIndex]) + val y2 = size.height - calculateHeight(points[roundedIndex + 1]) + val cx = (x1 + x2) / 2f + + val areaFraction = roundedIndex.toDouble() / (points.size - 1) + + val t = (fraction - areaFraction) * (points.size - 1) + + val outputY = ((1 - t).pow(3) * (y1) + + 3 * t * (1 - t).pow(2) * (y1) + + 3 * (1 - t) * t.pow(2) * (y2) + + t.pow(3) * y2).toFloat() + val outputX = ((1 - t).pow(3) * (x1) + + 3 * t * (1 - t).pow(2) * (cx) + + 3 * (1 - t) * t.pow(2) * (cx) + + t.pow(3) * x2).toFloat() + val calculatedValue = calculateValue( + minValue = minValue, + maxValue = maxValue, + total = size.height, + offset = size.height - outputY + ) + + println("$outputY,$outputX") + Value(calculatedValue = calculatedValue, offset = Offset(x = outputX, y = outputY)) + } else { + val p1 = points[roundedIndex] + val p2 = points.getOrNull(roundedIndex + 1) ?: p1 + val calculatedValue = ((p2 - p1) * (index - roundedIndex) + p1) + val offset = Offset( + x = if (points.count() > 1) (fraction * size.width).toFloat() else 0f, + y = size.height - calculateOffset( + minValue = minValue, + maxValue = maxValue, + total = size.height, + value = calculatedValue.toFloat() + ).toFloat() + ) + Value(calculatedValue = calculatedValue, offset = offset) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/CircularStepExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/CircularStepExtension.kt new file mode 100644 index 0000000..43a5157 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/CircularStepExtension.kt @@ -0,0 +1,43 @@ +package com.laseroptek.raman.utils.ext + +// 각 단계 (0 .. N-1) 에 따른 해당 각도를 얻는다 (각도는 0도 ~ 270도를 기준으로 함) +// - 예) 0단계 = 15도, 1단계 = 30도, .. +// steps: 총 단계 수 (예: 12단계: 0 ~ 11 단계) +fun Int.stepToDegree(totalSteps: Int, maxDegree: Float = 270f): Float { + if (totalSteps < 2) { + return 0f + } + + val oneStepDegree = maxDegree / (totalSteps - 1) + val degree = this * oneStepDegree + + //Timber.d("degree: ${degree} - (this(step): ${this}, totalSteps: ${totalSteps})") + + return degree +} + +// 해당 각도에 대한 단계 size에 대한 index(0 .. size-1)값을 얻는다. (각도는 0도 ~ 270도를 기준으로 함) +// - (예: 0도 ~ 14.9도: 1단계, 15도 ~ 29.9도: 2단계) +// steps: 총 단계 수 +fun Float.degreeToStep(totalSteps: Int, maxDegree: Float = 270f): Int { + if (totalSteps < 2) { + return 0 + } + + val oneStepDegree = maxDegree / (totalSteps - 1) + val halfDegree = oneStepDegree / 2 + + val intStep = (this / oneStepDegree).toInt() + val remainedDegree = this - intStep * oneStepDegree + + val step = if (remainedDegree >= halfDegree) + intStep + 1 + else + intStep + + //Timber.d("degreeToStep-> degree: ${this}, totalSteps: ${totalSteps}") + //Timber.d("degreeToStep-> intStep: ${intStep}, remainedDegree: ${remainedDegree}, halfDegree: ${halfDegree}") + //Timber.d("degreeToStep-> step: ${step}") + + return step +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/ContextExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/ContextExtension.kt new file mode 100644 index 0000000..25f004d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/ContextExtension.kt @@ -0,0 +1,11 @@ +package com.laseroptek.raman.utils.ext + +import android.content.Context +import android.content.ContextWrapper +import androidx.activity.ComponentActivity + +fun Context.getActivity(): ComponentActivity? = when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/DataClassExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/DataClassExtension.kt new file mode 100644 index 0000000..5b4fd51 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/DataClassExtension.kt @@ -0,0 +1,32 @@ +package com.laseroptek.raman.utils.ext + +import com.laseroptek.raman.data.model.SerialNumber + +fun SerialNumber.toStringRepresentation(): String { + return listOf(char1, char2, char3, char4, char5, char6).joinToString("") +} + +fun SerialNumber.toDashedStringRepresentation(): String { + return listOf(char1, char2, '-', char3, char4, char5, '-', char6).joinToString("") +} + +fun SerialNumber.toFullStringRepresentation(): String { + return "LO" + "-" + "3ND" + "-" + listOf(char1, char2, '-', char3, char4, char5, '-', char6).joinToString("") +} + +fun List.toFullStringRepresentation(): String { + // Safety check to ensure the list has enough elements to prevent a crash + if (this.size < 6) { + return "LO-3ND-IN-VAL-ID" // Return a default or error string + } + + // Use the elements from the list to construct the dashed representation + val dashedPart = listOf( + this[0], this[1], '-', + this[2], this[3], this[4], '-', + this[5] + ).joinToString("") + + // Combine with the prefix + return "LO-3ND-$dashedPart" +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/DateExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/DateExtension.kt new file mode 100644 index 0000000..7ec5241 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/DateExtension.kt @@ -0,0 +1,124 @@ +package com.laseroptek.raman.utils.ext + +import java.text.SimpleDateFormat +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Calendar +import java.util.Date +import java.util.Locale + +/** + * Pattern: yyyy-MM-dd HH:mm:ss + */ +fun Date.formatToServerDateTimeDefaults(): String{ + val sdf= SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + return sdf.format(this) +} + +fun Date.formatToTruncatedDateTime(): String{ + val sdf= SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()) + return sdf.format(this) +} + +/** + * Pattern: yyyy-MM-dd + */ +fun Date.formatToServerDateDefaults(): String{ + val sdf= SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + return sdf.format(this) +} + +/** + * Pattern: HH:mm:ss + */ +fun Date.formatToServerTimeDefaults(): String{ + val sdf= SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + return sdf.format(this) +} + +/** + * Pattern: dd/MM/yyyy HH:mm:ss + */ +fun Date.formatToViewDateTimeDefaults(): String{ + val sdf= SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.getDefault()) + return sdf.format(this) +} + +/** + * Pattern: dd/MM/yyyy + */ +fun Date.formatToViewDateDefaults(): String{ + val sdf= SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + return sdf.format(this) +} + +/** + * Pattern: HH:mm:ss + */ +fun Date.formatToViewTimeDefaults(): String{ + val sdf= SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + return sdf.format(this) +} + +/** + * Add field date to current date + */ +fun Date.add(field: Int, amount: Int): Date { + Calendar.getInstance().apply { + time = this@add + add(field, amount) + return time + } +} + +fun Date.addYears(years: Int): Date{ + return add(Calendar.YEAR, years) +} +fun Date.addMonths(months: Int): Date { + return add(Calendar.MONTH, months) +} +fun Date.addDays(days: Int): Date{ + return add(Calendar.DAY_OF_MONTH, days) +} +fun Date.addHours(hours: Int): Date{ + return add(Calendar.HOUR_OF_DAY, hours) +} +fun Date.addMinutes(minutes: Int): Date{ + return add(Calendar.MINUTE, minutes) +} +fun Date.addSeconds(seconds: Int): Date{ + return add(Calendar.SECOND, seconds) +} + +// convert timestamp value to hh:mm string + +fun Long.toHHMMString(): String { + val localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) + val formatter = DateTimeFormatter.ofPattern("HH:mm") + return localDateTime.format(formatter) +} + +fun Long.toHHMMSSString(): String { + val localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) + val formatter = DateTimeFormatter.ofPattern("HH:mm:ss") + return localDateTime.format(formatter) +} + +fun Long.toYYYYMMDDHHMMSSString(): String { + val localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + return localDateTime.format(formatter) +} + +fun Long.toFormattedDateTimeString( + pattern: String = "yyyyMMdd HH:mm:ss", + zoneId: ZoneId = ZoneId.systemDefault(), + locale: Locale = Locale.getDefault() +): String { + val instant = Instant.ofEpochMilli(this) + val localDateTime = LocalDateTime.ofInstant(instant, zoneId) + val formatter = DateTimeFormatter.ofPattern(pattern, locale) + return localDateTime.format(formatter) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/DebugExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/DebugExtension.kt new file mode 100644 index 0000000..d44ceae --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/DebugExtension.kt @@ -0,0 +1,70 @@ +package com.laseroptek.raman.utils.ext + +/** + * Returns the string name of a constant from the CMD object that matches the given integer value. + * + * @param value The integer value of the command (e.g., 0x01, 0x03). + * @return The string name of the constant (e.g., "VERSION", "LASER_STATUS"), + * or "UNKNOWN_CMD" if not found. + */ +fun getCmdString(value: Int): String { + return when(value) { + 0x01 -> "(0x%02X) VERSION -------".format(value) + 0x03 -> "(0x%02X) LASER_STATUS --".format(value) + 0x05 -> "(0x%02X) PURGE_BUBBLE --".format(value) + 0x06 -> "(0x%02X) DCD_GAS -------".format(value) + 0x07 -> "(0x%02X) SPRAY_DCD -----".format(value) + 0x08 -> "(0x%02X) HAND_PIECE ----".format(value) + 0x09 -> "(0x%02X) GUIDE_BEAM ----".format(value) + 0x0A -> "(0x%02X) HP_SHOT_COUNT -".format(value) + 0x0B -> "(0x%02X) ENERGE_METER --".format(value) + 0x0C -> "(0x%02X) TEMPERATURE ---".format(value) + 0x0D -> "(0x%02X) OVEN ----------".format(value) + 0x0F -> "(0x%02X) Q_SWITCH ------".format(value) + 0x10 -> "(0x%02X) ERROR ---------".format(value) + 0x11 -> "(0x%02X) WARNING -------".format(value) + 0x14 -> "(0x%02X) ENERGY_DETECT -".format(value) + 0x20 -> "(0x%02X) UPDATE_FIRMWARE".format(value) + else -> "(0x%02X) UNKNOWN_CMD ---".format(value) + } +} + +/* +import com.laseroptek.raman.utils.ext.getCmdString // Don't forget to import it + +fun processCommand(commandCode: Int) { // e.g., commandCode is 0x01 + // Use the helper function to get the string name + val commandName = getCmdString(commandCode) // This will return "VERSION" + + // Now you can use it for logging + Timber.d("Received command: $commandName (Code: 0x${commandCode.toString(16)})") + + // You can also use it in a when statement for clarity if needed, + // though using the constants is still best practice for control flow. + when (commandCode) { + CMD.VERSION -> { + // ... handle version command + } + // ... other cases + } +} + +fun processCommand(commandCode: Int) { // e.g., commandCode is 0x01 + // Use the helper function to get the string name// ... inside a function in MainViewModel + val commandName = getCmdString(commandCode) // This will return "VERSION"fun processCommand(commandCode: Int) { // e.g., commandCode is 0x01 + // Use the helper function to get the string name + // Now you can use it for logging val commandName = getCmdString(commandCode) // This will return "VERSION" + Timber.d("Received command: $commandName (Code: 0x${commandCode.toString(16)})") + // Now you can use it for logging + // You can also use it in a when statement for clarity if needed, Timber.d("Received command: $commandName (Code: 0x${commandCode.toString(16)})") + // though using the constants is still best practice for control flow. + when (commandCode) { // You can also use it in a when statement for clarity if needed, + CMD.VERSION -> { // though using the constants is still best practice for control flow. + // ... handle version command when (commandCode) { + } CMD.VERSION -> { + // ... other cases // ... handle version command + } } +} // ... other cases + } +} +*/ \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/FlowExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/FlowExtension.kt new file mode 100644 index 0000000..93b9c4a --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/FlowExtension.kt @@ -0,0 +1,20 @@ +package com.laseroptek.raman.utils.ext + +import kotlinx.coroutines.DelicateCoroutinesApi // <-- Import this +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout + +@OptIn(DelicateCoroutinesApi::class) // <-- Add this annotation +fun Flow.toStateFlowBlocking(timeoutMillis: Long = 5000) = runBlocking { + withTimeout(timeoutMillis) { + stateIn( + GlobalScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = null // This will produce a StateFlow + ) + } +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/Format.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/Format.kt new file mode 100644 index 0000000..732080d --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/Format.kt @@ -0,0 +1,30 @@ +package com.laseroptek.raman.utils.ext + +import kotlin.math.pow +import kotlin.math.round + +fun Double.format(decimalPlaces: Int): String { + require(decimalPlaces >= 0) { "Decimal places must be non-negative." } + + if (decimalPlaces == 0) { + return this.toInt().toString() // Handle whole numbers efficiently + } + + val factor = 10.0.pow(decimalPlaces.toDouble()) + val roundedValue = round(this * factor) / factor + return roundedValue.toString() +} + +fun Int.formatWithThousandComma(): String { + val result = StringBuilder() + val size = this.toString().length + return if (size > 3) { + for (i in size - 1 downTo 0) { + result.insert(0, this.toString()[i]) + if ((i != size - 1) && i != 0 && (size - i) % 3 == 0) + result.insert(0, "\'") + } + result.toString() + } else + this.toString() +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/GridLines.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/GridLines.kt new file mode 100644 index 0000000..fef3ca3 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/GridLines.kt @@ -0,0 +1,72 @@ +package com.laseroptek.raman.utils.ext + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.laseroptek.raman.data.model.chart.DividerProperties +import com.laseroptek.raman.data.model.chart.GridProperties +import com.laseroptek.raman.data.model.chart.IndicatorPosition + + +fun DrawScope.drawGridLines( + dividersProperties: DividerProperties, + indicatorPosition: IndicatorPosition, + gridEnabled: Boolean, + xAxisProperties: GridProperties.AxisProperties, + yAxisProperties: GridProperties.AxisProperties, + size: Size? = null, + xPadding: Float = 0f, + yPadding: Float = 0f +) { + + val _size = size ?: this.size + + val xAxisPathEffect = xAxisProperties.style.pathEffect + val yAxisPathEffect = yAxisProperties.style.pathEffect + + + if (xAxisProperties.enabled && gridEnabled) { + for (i in 0 until xAxisProperties.lineCount) { + val y = _size.height.spaceBetween(itemCount = xAxisProperties.lineCount, index = i) + drawLine( + brush = xAxisProperties.color, + start = Offset(0f + xPadding, y + yPadding), + end = Offset(_size.width + xPadding, y + yPadding), + strokeWidth = xAxisProperties.thickness.toPx(), + pathEffect = xAxisPathEffect + ) + } + } + if (yAxisProperties.enabled && gridEnabled) { + for (i in 0 until yAxisProperties.lineCount) { + val x = _size.width.spaceBetween(itemCount = yAxisProperties.lineCount, index = i) + drawLine( + brush = xAxisProperties.color, + start = Offset(x + xPadding, 0f + yPadding), + end = Offset(x + xPadding, _size.height + yPadding), + strokeWidth = xAxisProperties.thickness.toPx(), + pathEffect = yAxisPathEffect + ) + } + } + if (dividersProperties.xAxisProperties.enabled && dividersProperties.enabled) { + val y = if (indicatorPosition == IndicatorPosition.Vertical.Top) 0f else _size.height + drawLine( + brush = dividersProperties.xAxisProperties.color, + start = Offset(0f + xPadding, y + yPadding), + end = Offset(_size.width + xPadding, y + yPadding), + strokeWidth = dividersProperties.xAxisProperties.thickness.toPx(), + pathEffect = dividersProperties.xAxisProperties.style.pathEffect + ) + } + if (dividersProperties.yAxisProperties.enabled && dividersProperties.enabled) { + val x = if (indicatorPosition == IndicatorPosition.Horizontal.End) _size.width else 0f + drawLine( + brush = dividersProperties.yAxisProperties.color, + start = Offset(x + xPadding, 0f + yPadding), + end = Offset(x + xPadding, _size.height + yPadding), + strokeWidth = dividersProperties.yAxisProperties.thickness.toPx(), + pathEffect = dividersProperties.yAxisProperties.style.pathEffect + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/GsonExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/GsonExtension.kt new file mode 100644 index 0000000..b106df8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/GsonExtension.kt @@ -0,0 +1,13 @@ +package com.laseroptek.raman.utils.ext + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +inline fun T.toJson(gson: Gson = Gson()): String { + return gson.toJson(this) +} + +inline fun String.fromJson(gson: Gson = Gson()): T { + val type = object : TypeToken() {}.type + return gson.fromJson(this, type) +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/LazyListExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/LazyListExtension.kt new file mode 100644 index 0000000..97d3e56 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/LazyListExtension.kt @@ -0,0 +1,7 @@ +package com.laseroptek.raman.utils.ext + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.derivedStateOf + +val LazyListState.isScrolled: Boolean + get() = derivedStateOf { firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0 }.value diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/LocalTimeExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/LocalTimeExtension.kt new file mode 100644 index 0000000..49d98a8 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/LocalTimeExtension.kt @@ -0,0 +1,9 @@ +package com.laseroptek.raman.utils.ext + +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +fun LocalTime.toFormattedString(): String { + val formatter = DateTimeFormatter.ofPattern("hh:mm a") + return this.format(formatter) +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/MapExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/MapExtension.kt new file mode 100644 index 0000000..df1c4ea --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/MapExtension.kt @@ -0,0 +1,9 @@ +package com.laseroptek.raman.utils.ext + +internal fun Set.addOrRemove(element: E): Set { + return this.toMutableSet().apply { + if (!add(element)) { + remove(element) + } + }.toSet() +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/ModifierExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/ModifierExtension.kt new file mode 100644 index 0000000..e0ae744 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/ModifierExtension.kt @@ -0,0 +1,315 @@ +package com.laseroptek.raman.utils.ext + + +import android.annotation.SuppressLint +import android.graphics.BlurMaskFilter +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.PaintingStyle +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + + +/* +Row( + modifier = Modifier.noRippleClickable { + expanded = !expanded + } + ) { } + */ +inline fun Modifier.noRippleClickable(crossinline onClick: ()->Unit): Modifier = composed { + clickable(indication = null, + interactionSource = remember { MutableInteractionSource() }) { + onClick() + } +} + +@Composable +fun Modifier.simpleVerticalScrollbar( + state: LazyListState, + width: Dp = 8.dp +): Modifier { + val targetAlpha = if (state.isScrollInProgress) 1f else 0f + val duration = if (state.isScrollInProgress) 150 else 500 + + val alpha by animateFloatAsState( + targetValue = targetAlpha, + animationSpec = tween(durationMillis = duration) + ) + + return drawWithContent { + drawContent() + + val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index + val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f + + // Draw scrollbar if scrolling or if the animation is still running and lazy column has content + if (needDrawScrollbar && firstVisibleElementIndex != null) { + val elementHeight = this.size.height / state.layoutInfo.totalItemsCount + val scrollbarOffsetY = firstVisibleElementIndex * elementHeight + val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight + + drawRect( + color = Color.Red, + topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY), + size = Size(width.toPx(), scrollbarHeight), + alpha = alpha + ) + } + } +} + + +fun Modifier.dropShadow( + shape: Shape, + color: Color = Color.Black.copy(0.25f), + blur: Dp = 4.dp, + offsetY: Dp = 4.dp, + offsetX: Dp = 0.dp, + spread: Dp = 0.dp +) = this.then( + Modifier.drawBehind { + val transparentColor = color.copy(alpha = 0.0f).toArgb() + val shadowColor = color.copy(alpha = 1f).toArgb() + this.drawIntoCanvas { + val paint = Paint() + val frameworkPaint = paint.asFrameworkPaint() + frameworkPaint.color = transparentColor + frameworkPaint.setShadowLayer( + blur.toPx(), + offsetX.toPx(), + offsetY.toPx(), + shadowColor + ) + when (val outline = shape.createOutline(this.size, layoutDirection, this)) { + is Outline.Rounded -> { + it.drawRoundRect( + 0f, + 0f, + this.size.width, + this.size.height, + outline.roundRect.topLeftCornerRadius.x, + outline.roundRect.bottomRightCornerRadius.y, + paint + ) + } + else -> { + it.drawRect( + 0f, + 0f, + this.size.width, + this.size.height, + paint + ) + } + } + } + } +) + +fun Modifier.innerShadow( + shape: Shape, + color: Color, + blur: Dp, + offsetY: Dp, + offsetX: Dp, + spread: Dp +) = drawWithContent { + drawContent() + + val rect = Rect(Offset.Zero, size) + val paint = Paint().apply { + this.color = color + this.isAntiAlias = true + } + + val shadowOutline = shape.createOutline(size, layoutDirection, this) + + drawIntoCanvas { canvas -> + canvas.saveLayer(rect, paint) + canvas.drawOutline(shadowOutline, paint) + + val frameworkPaint = paint.asFrameworkPaint() + frameworkPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + if (blur.toPx() > 0) { + frameworkPaint.maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL) + } + paint.color = Color.Black + + val spreadOffsetX = offsetX.toPx() + if (offsetX.toPx() < 0) -spread.toPx() else spread.toPx() + val spreadOffsetY = offsetY.toPx() + if (offsetY.toPx() < 0) -spread.toPx() else spread.toPx() + + canvas.translate(spreadOffsetX, spreadOffsetY) + canvas.drawOutline(shadowOutline, paint) + canvas.restore() + } + +} + +enum class ButtonState { Pressed, Idle } +fun Modifier.bounceClick() = composed { + val buttonState: MutableState = remember { mutableStateOf(ButtonState.Idle) } + val scale = animateFloatAsState( if(buttonState.value == ButtonState.Pressed) 0.70f else 1f) + + this + .graphicsLayer { + scaleX = scale.value + scaleY = scale.value + } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { } + ) + .pointerInput(buttonState) { + awaitPointerEventScope { + buttonState.value = if (buttonState.value == ButtonState.Pressed) { + waitForUpOrCancellation() + ButtonState.Idle + } else { + awaitFirstDown(false) + ButtonState.Pressed + } + } + } +} + + +fun Modifier.dashedBorder( + width: Dp, + radius: Dp, + color: Color +) = drawBehind { + drawIntoCanvas { + val paint = Paint() + .apply { + strokeWidth = width.toPx() + this.color = color + style = PaintingStyle.Stroke + pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) + } + it.drawRoundRect( + width.toPx(), + width.toPx(), + size.width - width.toPx(), + size.height - width.toPx(), + radius.toPx(), + radius.toPx(), + paint + ) + } +} + +fun Modifier.shadowBottomEnd( + elevation: Dp, + color: Color = Color.Black.copy(alpha = 0.2f), + shadowRadius: Dp = elevation * 2, + offsetY: Dp = 0.dp, + offsetX: Dp = 0.dp +) = this.drawBehind { + val shadowPath = Path().apply { + moveTo(0f, size.height - offsetY.toPx()) + lineTo(size.width - offsetX.toPx(), size.height - offsetY.toPx()) + lineTo(size.width - offsetX.toPx(), size.height) + lineTo(0f, size.height) + close() + } + drawIntoCanvas { + it.drawPath( + path = shadowPath, + paint = Paint().apply { + this.style = PaintingStyle.Stroke + this.strokeWidth = shadowRadius.toPx() + this.color = color + } + ) + } +} + +fun Modifier.shadowBottomGradation( + elevation: Dp, + color: Color = Color.Black.copy(alpha = 0.2f), + shadowRadius: Dp = elevation * 2, + offsetY: Dp = 0.dp, + offsetX: Dp = 0.dp +) = this.drawBehind { + val shadowPath = Path().apply { + moveTo(0f, size.height - offsetY.toPx()) + lineTo(size.width - offsetX.toPx(), size.height - offsetY.toPx()) + lineTo(size.width - offsetX.toPx(), size.height) + lineTo(0f, size.height) + close() + } + val gradientShader: Shader = LinearGradientShader( + from = Offset(0f, size.height - offsetY.toPx()), + to = Offset(0f, size.height), + colors = listOf(color, Color.Transparent), + tileMode = TileMode.Clamp + ) + drawIntoCanvas { + it.drawPath( + path = shadowPath, + paint = Paint().apply { + this.style = PaintingStyle.Stroke + this.strokeWidth = shadowRadius.toPx() + this.shader = gradientShader + } + ) + } +} + +@SuppressLint("ModifierFactoryUnreferencedReceiver") +fun Modifier.detectBackgroundTap( + onTap: () -> Unit +): Modifier = this.pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + // Wait for any pointer event + val event = awaitPointerEvent(PointerEventPass.Initial) + + // Check if it's a simple "down" event (the end of a tap) + if (event.type == PointerEventType.Press) { + // Execute the provided logic + onTap() + } + + // Important: We don't consume the event, so it continues + // to be processed by children like LaserControlView for dragging. + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/MultipreviewAnnotation.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/MultipreviewAnnotation.kt new file mode 100644 index 0000000..4ee70ab --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/MultipreviewAnnotation.kt @@ -0,0 +1,68 @@ +package com.laseroptek.raman.utils.ext + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview + +/** + * Add this multipreview annotation to a composable to render the composable in extra small and + * extra large font size. + * + * Read more in the [documentation](https://d.android.com/jetpack/compose/tooling#preview-multipreview) + */ +@Preview( + name = "small font", + group = "font scales", + fontScale = 0.5f, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Preview( + name = "large font", + group = "font scales", + fontScale = 1.5f, + device = "spec:width=1920dp,height=1080dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +annotation class FontScalePreviews + +/** + * Add this multipreview annotation to a composable to render the composable on various device + * sizes: phone, foldable, and tablet. + * + * Read more in the [documentation](https://d.android.com/jetpack/compose/tooling#preview-multipreview) + */ +@Preview( + name = "phone", + group = "devices", + device = "spec:width=360dp,height=640dp,dpi=480,isRound=false,chinSize=0dp,orientation=landscape" +) +@Preview( + name = "foldable", + group = "devices", + device = "spec:width=673dp,height=841dp,dpi=480,isRound=false,chinSize=0dp,orientation=landscape" +) +@Preview( + name = "tablet", + group = "devices", + device = "spec:width=1280dp,height=800dp,dpi=480,isRound=false,chinSize=0dp,orientation=landscape" +) +annotation class DevicePreviews + +/** + * Add this multipreview annotation to a composable to render the composable in various common + * configurations: + * - Dark theme + * - Small and large font size + * - various device sizes + * + * Read more in the [documentation](https://d.android.com/jetpack/compose/tooling#preview-multipreview) + * + * _Note: Combining multipreview annotations doesn't mean all the different combinations are shown. + * Instead, each multipreview annotation acts by its own and renders only its own variants._ + */ +@Preview( + name = "dark theme", + group = "themes", + uiMode = UI_MODE_NIGHT_YES +) +@FontScalePreviews +@DevicePreviews +annotation class CompletePreviews diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/MutableExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/MutableExtension.kt new file mode 100644 index 0000000..011f0d0 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/MutableExtension.kt @@ -0,0 +1,271 @@ +package com.laseroptek.raman.utils.ext + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import kotlinx.coroutines.flow.MutableStateFlow +import timber.log.Timber + + +fun List.convertListToMutableStateListBasedOnBoolean( + condition: Boolean +): SnapshotStateList { + return derivedStateOf { + Timber.d("condition: ${condition}") + if (condition) { + mutableStateListOf().also { mutableStateListOf -> + mutableStateListOf.addAll(this) + } + } else { + mutableStateListOf() + } + }.value +} + +fun List.convertListToMutableStateList( +): SnapshotStateList { + return derivedStateOf { + mutableStateListOf().also { mutableStateListOf -> + mutableStateListOf.addAll(this) + } + }.value +} + +/* toMutableStateMap +val originalMap = mapOf( + Pair(1.0f, 2) to 10, + Pair(3.0f, 4) to 20 + ) + + val convertedMap = originalMap.toMutableStateMap() + + // Modifying the convertedMap + convertedMap[Pair(1.0f, 2)] = mutableStateOf(15) + convertedMap[Pair(5.0f, 6)] = mutableStateOf(25) // Adding a new entry + + println(convertedMap) + // Output: {Pair(1.0, 2)=MutableState(value=15), Pair(3.0, 4)=MutableState(value=20), Pair(5.0, 6)=MutableState(value=25)} +*/ + +/* +fun Map, V>.toMutableStateMap(): MutableMap, MutableStateFlow> { + return this.mapValues { (_, value) -> MutableStateFlow(value) }.toMutableMap() +} +*/ + +fun List.toMutableStateFlow(): MutableStateFlow> { + return MutableStateFlow(this) +} + +fun Map, V>.getKey2ListForKey1(key1: K1): List { + return this.filterKeys { it.first == key1 }.keys.map { it.second } +} + +fun Map, V>.toFluenceMutableStateFlowMap(): MutableStateFlow, V>> { + val mutableMap = this.mapValues { (_, value) -> value }.toMutableMap() + return MutableStateFlow(mutableMap) +} + +fun Map, V>.toVoltageMutableStateFlowMap(): MutableStateFlow, MutableStateFlow>> { + val mutableMap = this.mapValues { (_, value) -> MutableStateFlow(value) }.toMutableMap() + return MutableStateFlow(mutableMap) +} + +fun Map, V>.getValue(key1: K1, key2: K2): V? { + return this[Pair(key1, key2)] +} + +fun MutableStateFlow, MutableStateFlow>>.toRegularMap(): Map, V> { + return this.value.mapValues { (_, valueFlow) -> valueFlow.value } +} + +fun MutableStateFlow, MutableStateFlow>>.setVoltageTable( + key: Pair, + newValue: V +) { + val currentMap = this.value.toMutableMap() // Create a mutable copy + currentMap[key] = MutableStateFlow(newValue) // Update the value for the key + this.value = currentMap // Update the StateFlow with the modified map +} + +fun MutableStateFlow, MutableStateFlow>>.getValueFromVoltageTable( + key: Pair +): V? { + return this.value[key]?.value +} + +fun MutableStateFlow, MutableStateFlow>>.increaseVoltageTableValue( + key: Pair, + increment: V +) where V : Number { + val currentMap = this.value.toMutableMap() + val currentValue = currentMap[key]?.value?.toDouble() ?: 0.0 // Get current value or 0 if null + @Suppress("UNCHECKED_CAST") + val newValue = (currentValue + increment.toDouble()).let { + when (increment) { + is Int -> it.toInt() + is Float -> it.toFloat() + is Double -> it + else -> throw IllegalArgumentException("Unsupported number type: ${increment::class.simpleName}") + } + } as V // Add increment and cast back to original type + + currentMap[key] = MutableStateFlow(newValue) + this.value = currentMap +} + +fun MutableStateFlow, MutableStateFlow>>.decreaseVoltageTableValue( + key: Pair, + increment: V +) where V : Number { + val currentMap = this.value.toMutableMap() + val currentValue = currentMap[key]?.value?.toDouble() ?: 0.0 // Get current value or 0 if null + @Suppress("UNCHECKED_CAST") + val newValue = (currentValue - increment.toDouble()).let { + when (increment) { + is Int -> it.toInt() + is Float -> it.toFloat() + is Double -> it + else -> throw IllegalArgumentException("Unsupported number type: ${increment::class.simpleName}") + } + } as V // Add increment and cast back to original type + + currentMap[key] = MutableStateFlow(newValue) + this.value = currentMap +} + +/* getMutableState +val myMap = mutableMapOf, MutableState>() +myMap[Pair(1.0f, 2.0f)] = mutableStateOf(10) + +val mutableState = getMutableState(myMap, 1.0f, 2.0f) + +if (mutableState != null) { + println(mutableState.value) // Output: 10 + mutableState.value = 15 + println(mutableState.value) // Output: 15 +} else { + println("Key not found in the map") +} +*/ +fun MutableMap, MutableState>.getMutableState(k1: K1, k2: K2): MutableState? { + return this[Pair(k1, k2)] +} + +/* getCMemberListWithAValue +val myMap = mutableMapOf, MutableState>() +myMap[Pair("apple", 1)] = mutableStateOf(1.0f) +myMap[Pair("apple", 2)] = mutableStateOf(2.0f) +myMap[Pair("banana", 1)] = mutableStateOf(3.0f) + +val appleStates = myMap.getCMemberListWithAValue("apple") + +// appleStates now contains: [MutableState(value=1.0), MutableState(value=2.0)] +*/ +fun MutableMap, MutableState>.getCMemberListWithAValue(k1: K1): List> { + return this.filter { (key, _) -> key.first == k1 } + .map { (_, value) -> value } +} + + +/* +val originalMap = mutableMapOf, MutableState>() +originalMap[Pair(1.0f, 2.0f)] = mutableStateOf(10) +originalMap[Pair(3.0f, 4.0f)] = mutableStateOf(20) + +val convertedMap = originalMap.toMutableIntMap() + +// convertedMap is now a MutableMap, Int> +println(convertedMap) // Output: {Pair(1.0, 2.0)=10, Pair(3.0, 4.0)=20} +*/ +fun MutableMap, MutableState>.toMutableIntMap(): MutableMap, V> { + return this.mapValues { it.value.value }.toMutableMap() +} + + + +/* deepCopy() +val originalMap = mutableMapOf, MutableState>() +originalMap[Pair(1.0f, 2.0f)] = mutableStateOf(10) +originalMap[Pair(3.0f, 4.0f)] = mutableStateOf(20) + +val clonedMap = originalMap.deepCopy() + +// Modify the original map +originalMap[Pair(1.0f, 2.0f)]?.value = 15 + +// The cloned map remains unchanged +println(clonedMap[Pair(1.0f, 2.0f)]?.value) // Output: 10 +println(originalMap[Pair(1.0f, 2.0f)]?.value) // Output: 15 +*/ +fun MutableMap>.deepCopy(): MutableMap> { + val newMap = mutableMapOf>() + for ((key, value) in this) { + newMap[key] = mutableStateOf(value.value) + } + return newMap +} + + +fun Map, V>.toVoltageMutableMap(): MutableMap, MutableStateFlow> { + return this.mapValues { (_, value) -> MutableStateFlow(value) }.toMutableMap() +} + + +fun MutableMap, MutableStateFlow>.setVoltageTable( + key: Pair, + newValue: V +) { + this[key]?.value = newValue // Update the value for the key +} + +fun MutableMap, MutableStateFlow>.getValueFromVoltageTable( + key: Pair +): V? { + return this[key]?.value +} + +fun MutableMap, MutableStateFlow>.setVoltageTableValue( + key: Pair, + value: V +) where V : Number { + this[key]?.value = value +} + +fun MutableMap, MutableStateFlow>.increaseVoltageTableValue( + key: Pair, + increment: V +) where V : Number { + val currentValue = this[key]?.value?.toDouble() ?: 0.0 // Get current value or 0 if null + @Suppress("UNCHECKED_CAST") + val newValue = (currentValue + increment.toDouble()).let { + when (increment) { + is Int -> it.toInt() + is Float -> it.toFloat() + is Double -> it + else -> throw IllegalArgumentException("Unsupported number type: ${increment::class.simpleName}") + } + } as V // Add increment and cast back to original type + + this[key]?.value = newValue +} + +fun MutableMap, MutableStateFlow>.decreaseVoltageTableValue( + key: Pair, + decrement: V +) where V : Number { + val currentValue = this[key]?.value?.toDouble() ?: 0.0 // Get current value or 0 if null + @Suppress("UNCHECKED_CAST") + val newValue = (currentValue - decrement.toDouble()).let { + when (decrement) { // Use "decrement" here as well + is Int -> it.toInt() + is Float -> it.toFloat() + is Double -> it + else -> throw IllegalArgumentException("Unsupported number type: ${decrement::class.simpleName}") // And here + } + } as V // Cast back to original type + + this[key]?.value = newValue // Update the MutableStateFlow's value +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/PacketExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/PacketExtension.kt new file mode 100644 index 0000000..38696a9 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/PacketExtension.kt @@ -0,0 +1,618 @@ +package com.laseroptek.raman.utils.ext + +import com.laseroptek.raman.const.LASER_STATUS +import com.laseroptek.raman.const.TX_RX +import com.laseroptek.raman.data.model.SerialResult +import com.laseroptek.raman.data.model.serial.Alert +import com.laseroptek.raman.data.model.serial.DcdGas +import com.laseroptek.raman.data.model.serial.DcdGasOnOff +import com.laseroptek.raman.data.model.serial.EnergyControl +import com.laseroptek.raman.data.model.serial.EnergyHandpiece +import com.laseroptek.raman.data.model.serial.EnergyMeasured +import com.laseroptek.raman.data.model.serial.EnergyVersion +import com.laseroptek.raman.data.model.serial.GuideBeam +import com.laseroptek.raman.data.model.serial.HPShotCount +import com.laseroptek.raman.data.model.serial.HandPiece +import com.laseroptek.raman.data.model.serial.LaserStatus +import com.laseroptek.raman.data.model.serial.Oven +import com.laseroptek.raman.data.model.serial.Packet +import com.laseroptek.raman.data.model.serial.PurgeBubble +import com.laseroptek.raman.data.model.serial.QSwitch +import com.laseroptek.raman.data.model.serial.SprayDcd +import com.laseroptek.raman.data.model.serial.Temperature +import com.laseroptek.raman.data.model.serial.Version +import timber.log.Timber + +// convert Packet to ByteArray (Packet -> ByteArray) +// 1. 우선 stx ~ etx 까지 (데이터가 한번에 하나씩 오는 것으로 가정) +// 추후 예외 상황 발생시 아래 상황 처리 필요. +// 1) 한번 수신시 두개 이상의 stx ~ etx 가 있는 경우 +// 2) stx만 있는 경우. (중간에 잘려서 etx가 다음에 오는 경우) +// 3) 오류는 포맷이 맞지 않거나 기타. +fun ByteArray.toPacketResult(): SerialResult { + val stxIndex = this.indexOfFirst { it.toInt() == 0x21 } + val etxIndex = this.indexOfLast { it.toInt() == 0x0d } + if ((this.size >= 5) && (stxIndex != -1) && (etxIndex != 1) && ((etxIndex - stxIndex) >= 4) ) { + val packet = this.sliceArray(stxIndex..etxIndex).toPacket() + if (packet != null) { + val calCS = (packet.cmd + packet.data).toCS() + + Timber.d("packet cs: 0x%X, 0x%x}", packet.cs[0], packet.cs[1]) + Timber.d("calcul cs: 0x%X, 0x%x}", calCS[0], calCS[1]) + + if (packet.cs.contentEquals( calCS )) { + Timber.d("OK") + return SerialResult.Success(packet) + } else { + Timber.d("NOK") + } + } + } + + return SerialResult.Error(Exception("Packet Error")) +} + +// +// rxPacket() 에서 사용 +// + +private fun ByteArray.toPacket(): Packet? { + val packet = if (this.size > 4) + Packet( + this.sliceArray(0..0), // stx (0x21) + this.sliceArray(1..1), // cmd + if (this.size > 5) this.sliceArray(2..(this.size - 4)) else byteArrayOf(), // data + this.sliceArray((this.size - 3)..(this.size - 2)), // cs + this.sliceArray((this.size - 1)..(this.size - 1)), // etx (0x0D) + ) else null + return packet +} + +// Packet to ByteArray +fun Packet.toByteArray(): ByteArray { + val packet = this.stx + this.cmd + this.data + (this.cmd + this.data).toCS() + this.etx + return packet +} + +// get cs with (cmd + data) byte array to check packet is valid or not +// divide Byte with upper and lower 4 bit and ita (+0x30) each value +fun ByteArray.toCS(): ByteArray { + return this.sum().toByte().toHexCharBytes() +} + +// +// rxPacket data parsing +// + +fun ByteArray.toVersion(): Version { + val version = if (this.size == 16) { + Version( + String(this.sliceArray(0..3)), + String(this.sliceArray(4..7)), + String(this.sliceArray(8..11)), + String(this.sliceArray(12..15)), + ) + } else Version() + + return version +} + +fun ByteArray.toHpShotCount(): HPShotCount { + val hpCount = if (this.size == 8) { + HPShotCount( + String(this.sliceArray(0..7)).toIntOrNull() ?: 0 + ) + } else HPShotCount() + return hpCount +} + +fun ByteArray.toLaserStatus(): LaserStatus { + val laserStatus = + if (this.size == 1) { + LaserStatus( + this.sliceArray(0..0).toInt() + ) + } else if (this.size == 12) { + LaserStatus( + this.sliceArray(0..0).toInt() + , String(this.sliceArray(1..3)).toIntOrNull() ?: 0 + , String(this.sliceArray(4..7)).toFloatOrNull() ?: 00.0f + , String(this.sliceArray(8..11)).toFloatOrNull() ?: 0.0f + ) + } else LaserStatus() + return laserStatus +} + +fun ByteArray.toPurgeBubble(): PurgeBubble { + val purgeBubble = if (this.size == 1) { + PurgeBubble( + this.sliceArray(0..0).toInt() + ) + } else PurgeBubble() + return purgeBubble +} + +fun ByteArray.toDcdGas(): DcdGas { + val dcdGas = + if (this.size == 1) { + DcdGas( + this.sliceArray(0..0).toInt() + ) + } else if (this.size == 2) { + DcdGas( + status = this.sliceArray(0..0).toInt() + ,ok = this.sliceArray(1..1).toInt() + ) + } else if (this.size == 5) { + DcdGas( + this.sliceArray(0..0).toInt() + , String(this.sliceArray(1..3)).toFloatOrNull() ?: 00.0f + ) + } else DcdGas() + return dcdGas +} + +fun ByteArray.toSprayDcd(): SprayDcd { + val sprayDcd = if (this.size == 7) { + SprayDcd( + this.sliceArray(0..0).toInt() + , String(this.sliceArray(1..3)).toIntOrNull() ?: 0 + , String(this.sliceArray(4..6)).toIntOrNull() ?: 0 + ) + } else SprayDcd() + return sprayDcd +} + +fun ByteArray.toHandPiece(): HandPiece { + val handPiece = if (this.size == 2) { + HandPiece( + String(this.sliceArray(0..1)).toIntOrNull() ?: 0 + ) + } else HandPiece() + return handPiece +} + +fun ByteArray.toTemperature(): Temperature { + val temperature = if (this.size >= 35) { + Temperature( + String(this.sliceArray(0..3)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(4..7)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(8..11)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(12..15)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(16..19)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(20..23)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(24..27)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(28..31)).toFloatOrNull() ?: 00.0f + ,String(this.sliceArray(32..35)).toFloatOrNull() ?: 00.0f + ) + } else Temperature() + + return temperature +} + +fun ByteArray.toOven(): Oven { + val oven = if (this.size == 5) { + Oven( + String(this.sliceArray(0..3)).toByteOrNull() ?: 0 + ,String(this.sliceArray(4..7)).toFloatOrNull() ?: 00.0f + ) + } else Oven() + + return oven +} + +fun ByteArray.toQSwitch(): QSwitch { + val qswitch = if (this.size == 10) { + QSwitch( + String(this.sliceArray(0..4)).toFloatOrNull() ?: 000.0f + ,String(this.sliceArray(5..9)).toFloatOrNull() ?: 000.0f + ) + } else QSwitch() + + return qswitch +} + +// EneryDetect - type: 0x01 (Version) +fun ByteArray.toEnergyVersion(): EnergyVersion { + val energyVersion = if (this.size == 9) { + EnergyVersion( + this.sliceArray(0..0).toInt(), + String(this.sliceArray(1..4)), + String(this.sliceArray(5..8)) + ) + } else EnergyVersion() + + return energyVersion +} + +// EneryDetect - type: 0x02 (Remove the handpiece in mount)(Check if insert in mount) +fun ByteArray.toEnergyHandpiece(): EnergyHandpiece { + /* debug only + var hexData = "" + for (i in 0 .. this.size -1) { + hexData += "0x%02X ".format(this[i]) + } + Timber.d("size: ${this.size} hexData: $hexData") + Timber.d("status: ${this.sliceArray(1..1).toInt()} ") + */ + + val energyHandpiece = if (this.size == 2) { + EnergyHandpiece( + this.sliceArray(0..0).toInt(), + this.sliceArray(1..1).toInt() + ) + } else EnergyHandpiece() + + return energyHandpiece +} + +// EneryDetect - type: 0x03 (Control to detect energy) +fun ByteArray.toEnergyControl(): EnergyControl { + val energyControl = if (this.size == 2) { + EnergyControl( + this.sliceArray(0..0).toInt(), + this.sliceArray(1..1).toInt() + ) + } else EnergyControl() + + return energyControl +} + +// EneryDetect - type: 0x04 (Transmit the measured data) +fun ByteArray.toEnergyMeasured(): EnergyMeasured { + val energyMeasured = if (this.size == 2) { + EnergyMeasured( + this.sliceArray(0..0).toInt(), + this.sliceArray(1..1).toInt() + ) + } else if (this.size == 10) { + EnergyMeasured( + this.sliceArray(0..0).toInt(), + this.sliceArray(1..1).toInt(), + String(this.sliceArray(2..9)).toIntOrNull() ?: 0 + ) + } else if (this.size == 20) { + EnergyMeasured( + this.sliceArray(0..0).toInt(), + this.sliceArray(1..1).toInt(), + i = this.sliceArray(2..2).toInt(), + String(this.sliceArray(3..10)).toIntOrNull() ?: 0, + e = this.sliceArray(11..11).toInt(), + String(this.sliceArray(12..19)).toIntOrNull() ?: 0, + ) + } + else EnergyMeasured() + + return energyMeasured +} + +fun ByteArray.toError(): Alert { + val error = if (this.size == 2) { + Alert( + String(this.sliceArray(0..1)).toIntOrNull() ?: 0 + ) + } else Alert() + + return error +} + +fun ByteArray.toWarning(): Alert { + val alert = if (this.size == 2) { + Alert( + String(this.sliceArray(0..1)).toIntOrNull() ?: 0 + ) + } else Alert() + + return alert +} + + +fun ByteArray.printHex( + txRx: String = "-", + rw: String = "-", + cmd: String = "-" +) { + var hexStringPerLine = "" + + Timber.d("[%s] (%s) %s (%02d) Bytes ---------", + txRx, + rw, + cmd, + this.size + ) + + this.forEachIndexed { index, data -> + val hex = "%02X".format(data) + hexStringPerLine += hex + + if ((index+1) % 16 == 0 || (index+1) == this.size) { + // --- THE FIX --- + // Provide both arguments required by the format string: + // 1. The integer for "%02d" + // 2. The string for "%s" (using %s is safer than %2s if length varies) + Timber.d( "[%02d] %s", (index/16 + 1), hexStringPerLine ) + // --- END OF FIX --- + hexStringPerLine = "" // init + } else { + hexStringPerLine += " " + } + } + Timber.d("%s", "--".repeat(26)) + Timber.d("\n") +} + +fun ByteArray.getHexString( + txRx: TX_RX = TX_RX.NONE +) : String { + var hexStringPerLine = "" + + this.forEachIndexed { index, data -> + val hex = "%02X ".format(data) + hexStringPerLine += hex + } + + if (txRx == TX_RX.RX) { + hexStringPerLine = "RX: " + hexStringPerLine + } else if (txRx == TX_RX.TX) { + hexStringPerLine = "TX: " + hexStringPerLine + } else { + //hexStringPerLine = hexStringPerLine + } + + return hexStringPerLine +} + + +// +// txPacket data parsing +// + +fun LaserStatus.toByteArray(): ByteArray { + if (this.laserStatus == LASER_STATUS.READY) { + val onTimeIntegerPart = this.onTime.toInt() + val onTimeFractionPart = ((this.onTime - onTimeIntegerPart) * 10).toInt() + val frequenceIntegerPart = this.frequence.toInt() + val frequenceFractionPart = ((this.frequence - frequenceIntegerPart) * 10).toInt() + + val laserStatusArray = byteArrayOf(this.laserStatus.toByte()) + val vol3figureAsciiArray = byteArrayOf( + ((voltage.getNthDigit(2) + 0x30) and 0xFF).toByte(), + ((voltage.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((voltage.getNthDigit(0) + 0x30) and 0xFF).toByte(), + ) + val onTimeArray = byteArrayOf( + ((onTimeIntegerPart.getNthDigit(1) + 0x30) and 0xFF).toByte(), // 10의 자리 (100으로 나눈 나머지) + ((onTimeIntegerPart.getNthDigit(0) + 0x30) and 0xFF).toByte(), // 1의 자리 (10으로 나눈 나머지) + "."[0].code.toByte(), // ascii: 0x2E + ((onTimeFractionPart + 0x30) and 0xFF).toByte(), + ) + val frequenceArray = byteArrayOf( + ((frequenceIntegerPart.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((frequenceIntegerPart.getNthDigit(0) + 0x30) and 0xFF).toByte(), + "."[0].code.toByte(), // ascii: 0x2E + ((frequenceFractionPart + 0x30) and 0xFF).toByte() + ) + return laserStatusArray + vol3figureAsciiArray + onTimeArray + frequenceArray + } else { + return byteArrayOf(this.laserStatus.toByte()) + } +} + +fun EnergyControl.toByteArray(): ByteArray { + val typeArray = byteArrayOf(this.type.toByte()) + val statusArray = byteArrayOf(this.status.toByte()) + + if (voltage == 0) { + return typeArray + statusArray + } else { + val vol3figureAsciiArray = byteArrayOf( + ((voltage.getNthDigit(2) + 0x30) and 0xFF).toByte(), + ((voltage.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((voltage.getNthDigit(0) + 0x30) and 0xFF).toByte(), + ) + return typeArray + statusArray + vol3figureAsciiArray + } +} + +fun PurgeBubble.toByteArray(): ByteArray { + return byteArrayOf(this.value.toByte()) +} + +fun DcdGasOnOff.toByteArray(): ByteArray { + return byteArrayOf(this.status.toByte()) +} + +fun DcdGas.toByteArray(): ByteArray { + val pressureIntegerPart = this.pressure.toInt() + val pressureFractionPart = ((this.pressure - pressureIntegerPart) * 10).toInt() + + val onOffArray = byteArrayOf(this.status.toByte()) + val pressureArray = byteArrayOf( + ((pressureIntegerPart.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((pressureIntegerPart.getNthDigit(0) + 0x30) and 0xFF).toByte(), + "."[0].code.toByte(), // ascii: 0x2E + ((pressureFractionPart + 0x30) and 0xFF).toByte() + ) + val okArray = byteArrayOf(this.ok.toByte()) + + return if (this.ok == 0x00) { + (onOffArray + pressureArray) + } else { + (onOffArray + okArray) + } +} + +fun SprayDcd.toByteArray(): ByteArray { + val onOffArray = byteArrayOf(this.status.toByte()) + val sprayTimeArray = byteArrayOf( + ((this.sprayTime.getNthDigit(2) + 0x30) and 0xFF).toByte(), + ((this.sprayTime.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((this.sprayTime.getNthDigit(0) + 0x30) and 0xFF).toByte(), + ) + val sprayDelayArray = byteArrayOf( + ((this.sprayDelay.getNthDigit(2) + 0x30) and 0xFF).toByte(), + ((this.sprayDelay.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((this.sprayDelay.getNthDigit(0) + 0x30) and 0xFF).toByte(), + ) + return onOffArray + sprayTimeArray + sprayDelayArray +} + +fun GuideBeam.toByteArray(): ByteArray { + val arrayValue = byteArrayOf( + ((this.value.getNthDigit(3) + 0x30) and 0xFF).toByte(), + ((this.value.getNthDigit(2) + 0x30) and 0xFF).toByte(), + ((this.value.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((this.value.getNthDigit(0) + 0x30) and 0xFF).toByte(), + ) + + return arrayValue +} + +fun Oven.toByteArray(): ByteArray { + val ktpIntegerPart = this.ktp.toInt() + val ktpFractionPart = ((this.ktp - ktpIntegerPart) * 10).toInt() + + val typeArray = byteArrayOf(this.type) + val pressureArray = byteArrayOf( + ((ktpIntegerPart.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((ktpIntegerPart.getNthDigit(0) + 0x30) and 0xFF).toByte(), + "."[0].code.toByte(), // ascii: 0x2E + ((ktpFractionPart + 0x30) and 0xFF).toByte() + ) + + return typeArray + pressureArray +} + +fun QSwitch.toByteArray(): ByteArray { + val delayTimeIntegerPart = this.delayTime.toInt() + val delayTimeFractionPart = ((this.delayTime - delayTimeIntegerPart) * 10).toInt() + val intervalTimeIntegerPart = this.intervalTime.toInt() + val intervalTimeFractionPart = ((this.intervalTime - delayTimeIntegerPart) * 10).toInt() + + val delayTimeArray = byteArrayOf( + ((delayTimeIntegerPart.getNthDigit(2) + 0x30) and 0xFF).toByte(), + ((delayTimeIntegerPart.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((delayTimeIntegerPart.getNthDigit(0) + 0x30) and 0xFF).toByte(), + "."[0].code.toByte(), // ascii: 0x2E + ((delayTimeFractionPart + 0x30) and 0xFF).toByte() + ) + val intervalTimeArray = byteArrayOf( + ((intervalTimeIntegerPart.getNthDigit(2) + 0x30) and 0xFF).toByte(), + ((intervalTimeIntegerPart.getNthDigit(1) + 0x30) and 0xFF).toByte(), + ((intervalTimeIntegerPart.getNthDigit(0) + 0x30) and 0xFF).toByte(), + "."[0].code.toByte(), // ascii: 0x2E + ((intervalTimeFractionPart + 0x30) and 0xFF).toByte() + ) + + return delayTimeArray + intervalTimeArray +} + +// +// 공통 함수 +// + +// 정수에서 N 번쨰 자리 수 구하기 +fun Int.getNthDigit(n: Int): Int { + return (this / Math.pow(10.0, n.toDouble()) % 10).toInt() +} + +// display each versions +fun String.toVersionString(): String { + var version = "" + this.forEachIndexed { index, char -> + version += char.toString() + if (index < this.length - 1) { + version += "." + } + } + return version +} + +fun String.toVersionByteArray(): ByteArray { + var version = "" + this.forEachIndexed { index, char -> + version += char.toString() + if (char.toString() != "." ) { + version += char + } + } + return version.toByteArray() +} + +fun Int.toHex(int: Int): String { + return int.toString(16) +} + +fun Byte.toHexCharBytes(): ByteArray { + val hexString = "%02X".format(this) // Convert to hex string + val highNibble = hexString[0].toChar().code // Get ASCII code of high nibble + val lowNibble = hexString[1].toChar().code // Get ASCII code of low nibble + return byteArrayOf(highNibble.toByte(), lowNibble.toByte()) // Create byte array +} + +fun ByteArray.toInt(): Int { + require(this.size <= 4) { "ByteArray size must be at most 4 bytes" } + var result = 0 + for (i in this.indices) { + result = (result shl 8) or (this[i].toInt() and 0xFF) + } + return result +} + +fun Int.clearNthBit(nth: Int): Int { + val mask = (1 shl nth).inv() // Create a mask with the nth bit set to 0 + return this and mask // Apply the mask to clear the nth bit +} + +fun Int.mask(flag: Int): Int { + val mask = (flag).inv() // Create a mask with the nth bit set to 0 + return this and mask // Apply the mask to clear the nth bit +} + +// convert ByteArray to Version (No Send Version CMD) +// fun Version.toByteArray(): ByteArray { } + +/* +fun Byte.toHexString(): String { + return this.toUByte().toString(16).padStart(2, '0').uppercase() +} + +@OptIn(ExperimentalUnsignedTypes::class) +fun ULong.toByte12(): UByteArray { + var hex = this.toString() + var hexUByte:UByteArray = UByteArray(12) + + for(i in 0 until (12-hex.length)) + hex = "0" + hex + + for(i in 0 until hexUByte.size) + hexUByte[i] = hex[i].code.toUByte() + + return hexUByte +} + +@SuppressLint("DefaultLocale") +@OptIn(ExperimentalUnsignedTypes::class) +fun UShort.toByte4(): UByteArray { + val hex = String.format("%04d", this.toInt()) + var hexUByte:UByteArray = ubyteArrayOf(0x00u, 0x00u, 0x00u, 0x00u) + + hexUByte[0] = hex[0].code.toUByte() + hexUByte[1] = hex[1].code.toUByte() + hexUByte[2] = hex[2].code.toUByte() + hexUByte[3] = hex[3].code.toUByte() + return hexUByte +} + +@SuppressLint("DefaultLocale") +@OptIn(ExperimentalUnsignedTypes::class) +fun UShort.toByte3(): UByteArray { + val hex = String.format("%03d", this.toInt()) + var hexUByte:UByteArray = ubyteArrayOf(0x00u, 0x00u, 0x00u) + + hexUByte[0] = hex[0].code.toUByte() + hexUByte[1] = hex[1].code.toUByte() + hexUByte[2] = hex[2].code.toUByte() + return hexUByte +} +*/ \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/ResourcesExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/ResourcesExtension.kt new file mode 100644 index 0000000..8f46b6b --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/ResourcesExtension.kt @@ -0,0 +1,13 @@ +package com.laseroptek.raman.utils.ext + +import android.content.res.Resources +import androidx.annotation.StringRes +import androidx.core.text.parseAsHtml +import androidx.core.text.toHtml +import androidx.core.text.toSpanned + +/* get string resource with paramter + eg. resources.getText(R.string.my_text, "I WILL BE BOLDED") + */ +fun Resources.getText(@StringRes id: Int, vararg formatArgs: Any?): CharSequence = + getText(id).toSpanned().toHtml().format(*formatArgs).parseAsHtml() \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/Size.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/Size.kt new file mode 100644 index 0000000..4032dca --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/Size.kt @@ -0,0 +1,26 @@ +package com.laseroptek.raman.utils.ext + +import android.content.res.Resources +import androidx.compose.ui.unit.Dp + +fun Float.spaceBetween(itemCount: Int, index: Int): Float { + if (itemCount == 1) return 0f + val itemSize = this / (itemCount - 1) + val positions = (0 until itemCount).map { it * itemSize } + val result = positions[index] + return result +} + + +/** + * Convert px to dp (x 1.5 : 1280x720 to 1920x1080 = 1.5x) + * eg. Box(modifier = Modifier.width(100.px.dp)) + */ +val Int.px: Int + get() = (this * 1.5 / Resources.getSystem().displayMetrics.density).toInt() + +val Double.px: Int + get() = (this * 1.5 / Resources.getSystem().displayMetrics.density).toInt() + +val Dp.toIntPx: Int + get() = (this / Resources.getSystem().displayMetrics.density * 0.5f).value.toInt() \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/SoundExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/SoundExtension.kt new file mode 100644 index 0000000..ed016ec --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/SoundExtension.kt @@ -0,0 +1,17 @@ +package com.laseroptek.raman.utils.ext + +import android.content.Context +import android.media.AudioManager + +fun (() -> Unit).withSound(context: Context): () -> Unit = { + (context.getSystemService(Context.AUDIO_SERVICE) as AudioManager) + .playSoundEffect(AudioManager.FX_KEY_CLICK) + this() +} + +fun ((Int) -> Unit).withSound(context: Context): (Int) -> Unit = { + (context.getSystemService(Context.AUDIO_SERVICE) as AudioManager) + .playSoundEffect(AudioManager.FX_KEY_CLICK) + this( it ) +} + diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/StringExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/StringExtension.kt new file mode 100644 index 0000000..1fe19b5 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/StringExtension.kt @@ -0,0 +1,32 @@ +package com.laseroptek.raman.utils.ext + +import java.util.regex.Pattern + +fun String.formatWithDots(): String { + if (this.isEmpty()) { + return this + } + + // Regex to capture the initial numeric part and the subsequent alphabetic/symbol part + // (\d+) captures one or more digits at the beginning. + // ([a-zA-Z_].*)? captures an optional group starting with a letter or underscore, followed by anything. + val pattern = Pattern.compile("^(\\d+)([a-zA-Z_].*)?$") + val matcher = pattern.matcher(this) + + if (matcher.matches()) { + val numericPart = matcher.group(1) ?: "" // Should always exist if matcher.matches() is true + val alphaPart = matcher.group(2) ?: "" // Optional part + + // Insert dots between digits + val formattedNumericPart = numericPart.toCharArray().joinToString(".") + + return when { + formattedNumericPart.isNotEmpty() && alphaPart.isNotEmpty() -> "$formattedNumericPart.$alphaPart" + formattedNumericPart.isNotEmpty() -> formattedNumericPart + else -> this // Should not happen if numericPart was matched, but as a fallback + } + } else { + // String does not start with a digit as per the pattern, or is only alphabetic + return this + } +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/slider/LabeledRangeSlider.kt b/app/src/main/java/com/laseroptek/raman/utils/slider/LabeledRangeSlider.kt new file mode 100644 index 0000000..a724a90 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/slider/LabeledRangeSlider.kt @@ -0,0 +1,374 @@ +package com.laseroptek.raman.utils.slider + +import android.content.res.Configuration +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.laseroptek.raman.utils.ext.px +import timber.log.Timber +import kotlin.math.abs + +@Composable +fun LabeledRangeSlider( + selectedUpperBound: T, + steps: List, + onRangeChanged: (upper: T) -> Unit, // Called during drag for live updates + onRangeChangeFinished: ((upper: T) -> Unit)? = null, // Called when touch is released + modifier: Modifier = Modifier, + sliderConfig: SliderConfiguration = SliderConfiguration() +) { + require(steps.size > 0) { "List of steps has to be at least of size 1" } + + var touchInteractionState by remember { mutableStateOf(TouchInteraction.NoInteraction) } + var moveSelector by remember { mutableStateOf(false) } + var composableSize by remember { mutableStateOf(IntSize(0, 0)) } + val currentDensity = LocalDensity.current + + val heightInDp = remember(key1 = sliderConfig, key2 = currentDensity) { + // Calculate height in Dp for the Modifier.height + with(currentDensity) { + (sliderConfig.touchCircleRadius.toPx() * 2 + sliderConfig.textSize.toPx() + sliderConfig.textOffset.toPx()).toDp() + } + } + + val barYCenter = remember(composableSize, sliderConfig, currentDensity) { + composableSize.height - sliderConfig.touchCircleRadiusPx(currentDensity) // Px value + } + val barXStart = 0f + val barYStart = remember(barYCenter, sliderConfig, currentDensity) { + barYCenter - sliderConfig.barHeightPx(currentDensity) / 2 // Px value + } + val barWidth = remember(composableSize) { + composableSize.width.toFloat() - 2 * barXStart // Px value + } + val barWidthDummy = 30.0f // draw larger than real barWidth + val barCornerRadius = remember(sliderConfig, currentDensity) { + // CornerRadius expects Px values + CornerRadius(sliderConfig.barCornerRadiusPx(currentDensity), sliderConfig.barCornerRadiusPx(currentDensity)) + } + + val (stepXCoordinates, stepSpacing) = remember(composableSize, steps, sliderConfig, currentDensity, barWidth) { + if (composableSize.width == 0) { + Pair(FloatArray(0), 0f) + } else { + calculateStepCoordinatesAndSpacing( + numberOfSteps = steps.size, + barXStart = barXStart, + barWidth = barWidth, + sliderConfig = sliderConfig, + density = currentDensity + ) + } + } + + var rightCirclePosition by remember { mutableStateOf(Offset(0f, 0f)) } + + LaunchedEffect(selectedUpperBound, composableSize, steps, stepXCoordinates, barYCenter) { + if (composableSize != IntSize(0,0) && steps.isNotEmpty() && stepXCoordinates.isNotEmpty()) { + val upperBoundIdx = steps.indexOf(selectedUpperBound) + if (upperBoundIdx != -1 && upperBoundIdx < stepXCoordinates.size) { + rightCirclePosition = Offset(stepXCoordinates[upperBoundIdx], barYCenter) + } else { + val closestStepIndex = steps.indices.minByOrNull { abs(steps[it].toFloat() - selectedUpperBound.toFloat()) } ?: 0 + if (closestStepIndex < stepXCoordinates.size) { + rightCirclePosition = Offset(stepXCoordinates[closestStepIndex], barYCenter) + } + } + } + } + + Canvas( + modifier = modifier + .height(heightInDp) // Use Dp for layout height + .onSizeChanged { + composableSize = it + } + .touchInteraction(remember { MutableInteractionSource() }) { + touchInteractionState = it + } + ) { + if (stepXCoordinates.isEmpty() || (rightCirclePosition == Offset(0f,0f) && composableSize == IntSize(0,0))) { + return@Canvas + } + + drawRoundRect( + color = sliderConfig.barColor, + topLeft = Offset(barXStart, barYStart), + size = Size(barWidth + barWidthDummy, sliderConfig.barHeightPx(this)), // 'this' is DrawScope (Density) + cornerRadius = barCornerRadius // Already in Px + ) + + drawRect( + color = sliderConfig.barColorInRange, + topLeft = Offset(barXStart, barYStart), + size = Size(rightCirclePosition.x - barXStart, sliderConfig.barHeightPx(this)) // 'this' is DrawScope + ) + + drawStepMarkersAndLabels( + steps = steps, + stepXCoordinates = stepXCoordinates, + rightCirclePosition = rightCirclePosition, + barYCenter = barYCenter, + sliderConfig = sliderConfig, + density = this // Pass DrawScope's density + ) + + drawCircleWithShadow( + position = rightCirclePosition, + touched = moveSelector, + sliderConfig = sliderConfig, + density = this // Pass DrawScope's density + ) + } + + handleTouch( + leftCirclePosition = barXStart, + rightCirclePosition = rightCirclePosition, + moveSelector = moveSelector, + stepXCoordinates = stepXCoordinates, + stepSpacing = stepSpacing, + touchInteraction = touchInteractionState, + updateSelector = { position, move -> + val (_, closestIndex) = stepXCoordinates.getClosestNumber(position.x) + if (closestIndex >= 0 && closestIndex < steps.size) { + onRangeChanged(steps[closestIndex]) + } + moveSelector = move + }, + onTouchInteractionChanged = { touchInteractionState = it }, + onRangeIdxChanged = { upperBoundIdx -> + if (upperBoundIdx >= 0 && upperBoundIdx < steps.size) { + onRangeChanged(steps[upperBoundIdx]) + onRangeChangeFinished?.invoke(steps[upperBoundIdx]) + } + } + ) +} + +private fun calculateStepCoordinatesAndSpacing( + numberOfSteps: Int, + barXStart: Float, + barWidth: Float, + sliderConfig: SliderConfiguration, + density: Density +): Pair { + if (numberOfSteps <= 0) return Pair(FloatArray(0), 0f) + if (numberOfSteps == 1) { + val singleStepX = barXStart + barWidth / 2 + return floatArrayOf(singleStepX) to 0f + } + // val stepMarkerRadius = sliderConfig.stepMarkerRadiusPx(density) // Not directly used for spacing here + val stepSpacing = barWidth / (numberOfSteps - 1) + + val stepXCoordinates = generateSequence(barXStart) { it + stepSpacing } + .take(numberOfSteps) + .toList() + + return stepXCoordinates.toFloatArray() to stepSpacing +} + +private fun DrawScope.drawCircleWithShadow( + position: Offset, + touched: Boolean, + sliderConfig: SliderConfiguration, + density: Density // Parameter name matches, but DrawScope is already a Density +) { + val touchAddition = if (touched) { + sliderConfig.touchCircleShadowTouchedSizeAdditionPx(density) + } else { + 0f + } + + drawIntoCanvas { + val paint = androidx.compose.ui.graphics.Paint() + val frameworkPaint = paint.asFrameworkPaint() + frameworkPaint.color = sliderConfig.touchCircleColor.toArgb() + frameworkPaint.setShadowLayer( + sliderConfig.touchCircleShadowSizePx(density) + touchAddition, + 0f, + 0f, + Color.DarkGray.toArgb() + ) + it.drawCircle( + position, + sliderConfig.touchCircleRadiusPx(density), + paint + ) + } +} + +private fun DrawScope.drawStepMarkersAndLabels( + steps: List, + stepXCoordinates: FloatArray, + rightCirclePosition: Offset, + barYCenter: Float, + sliderConfig: SliderConfiguration, + density: Density // Parameter name matches, but DrawScope is already a Density +) { + if (steps.size != stepXCoordinates.size) { + Timber.e("Step value size and step coordinate size do not match. Value size: ${steps.size}, Coordinate size: ${stepXCoordinates.size}") + return + } + val stepMarkerRadius = sliderConfig.stepMarkerRadiusPx(density) + sliderConfig.textSizePx(density) + val textOffsetY = with(density) { sliderConfig.textOffset.toPx() } + + steps.forEachIndexed { index, step -> + if (index >= stepXCoordinates.size) return@forEachIndexed + val stepMarkerCenter = Offset(stepXCoordinates[index], barYCenter) + + val isCurrentlySelectedBySelectorCircle = + (rightCirclePosition.x >= (stepMarkerCenter.x - stepMarkerRadius / 2)) && + (rightCirclePosition.x < (stepMarkerCenter.x + stepMarkerRadius / 2)) + + val paint = when { + isCurrentlySelectedBySelectorCircle -> sliderConfig.textSelectedPaint(density) + stepMarkerCenter.x > rightCirclePosition.x -> sliderConfig.textOutOfRangePaint(density) + else -> sliderConfig.textInRangePaint(density) + } + + drawCircle( + color = sliderConfig.stepMarkerColor, + radius = stepMarkerRadius, + alpha = .1f, + center = stepMarkerCenter + ) + + drawIntoCanvas { + /* + val stepText = step.toString().let { text -> + if (text.length > 3) { + text.substring(0, text.length.coerceAtMost(2)) + } else { + text + } + } + Timber.d("stepText: $stepText") + */ + + val stepText = "•" // • // ● + + // Adjust text Y position to be above the slider bar center + val textYPosition = barYCenter - stepMarkerRadius - textOffsetY // Adjust based on needs + it.nativeCanvas.drawText( + stepText, + stepMarkerCenter.x - (paint.measureText(stepText) / 2), // Center text horizontally + textYPosition, + paint + ) + } + } +} + +private fun handleTouch( + leftCirclePosition: Float, + rightCirclePosition: Offset, + moveSelector: Boolean, + stepXCoordinates: FloatArray, + stepSpacing: Float, + touchInteraction: TouchInteraction, + updateSelector: (Offset, Boolean) -> Unit, + onTouchInteractionChanged: (TouchInteraction) -> Unit, + onRangeIdxChanged: (Int) -> Unit +) { + if (stepXCoordinates.isEmpty()) return + + when (touchInteraction) { + is TouchInteraction.Move -> { + val touchPositionX = touchInteraction.position.x + val newSelectorX = touchPositionX.coerceIn(stepXCoordinates.first(), stepXCoordinates.last()) + val newPosition = rightCirclePosition.copy(x = newSelectorX) + updateSelector(newPosition, true) + } + is TouchInteraction.Up -> { + val (closestSelectorValue, closestSelectorIndex) = stepXCoordinates.getClosestNumber(rightCirclePosition.x) + val finalPosition = rightCirclePosition.copy(x = closestSelectorValue) + updateSelector(finalPosition, false) + if (closestSelectorIndex != -1) { + onRangeIdxChanged(closestSelectorIndex) + } + onTouchInteractionChanged(TouchInteraction.NoInteraction) + } + else -> { + // NoInteraction or Down. + } + } +} + +private fun FloatArray.getClosestNumber(input: Float): Pair { + if (this.isEmpty()) return 0f to -1 + + var minElem = this[0] + var minValue = abs(minElem - input) + var minIdx = 0 + + for (i in 0..lastIndex) { + val e = this[i] + val v = abs(e - input) + if (v < minValue) { + minElem = e + minValue = v + minIdx = i + } else if (v == minValue) { + if (abs(e - input) < abs(minElem - input)) { + minElem = e + minIdx = i + } + } + } + return minElem to minIdx +} + +@Preview( + apiLevel = 34, + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = false, + device = "spec:width=640dp,height=240dp,dpi=150,isRound=false,chinSize=0dp,orientation=landscape" +) +@Composable +fun CustomSliderPreview() { + Row( + modifier = Modifier + .fillMaxSize() + .padding(34.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LabeledRangeSlider( + selectedUpperBound = 5, + steps = (0..10).step(1).toList(), + onRangeChanged = {}, + onRangeChangeFinished = {}, + modifier = Modifier + .fillMaxHeight() + .width(260.px.dp) + .padding(start = 16.dp, end = 16.dp) + ) + } +} diff --git a/app/src/main/java/com/laseroptek/raman/utils/slider/SliderConfiguration.kt b/app/src/main/java/com/laseroptek/raman/utils/slider/SliderConfiguration.kt new file mode 100644 index 0000000..c34e782 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/slider/SliderConfiguration.kt @@ -0,0 +1,51 @@ +package com.laseroptek.raman.utils.slider + +import android.graphics.Paint +import android.graphics.Typeface +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +data class SliderConfiguration( + val barHeight: Dp = 12.dp, + val barColor: Color = Color.LightGray, + val barColorInRange: Color = Color.Black, + val barCornerRadius: Dp = 6.dp, + val touchCircleRadius: Dp = 16.dp, + val touchCircleShadowSize: Dp = 4.dp, + val touchCircleShadowTouchedSizeAddition: Dp = 2.dp, + val touchCircleColor: Color = Color.White, + val textColorInRange: Color = Color.Black, + val textColorOutOfRange: Color = Color.LightGray, + val textSize: TextUnit = 16.sp, + val textOffset: Dp = 8.dp, + val stepMarkerColor: Color = Color.DarkGray, +) { + internal fun barHeightPx(density: Density): Float = with(density) { barHeight.toPx() } + internal fun barCornerRadiusPx(density: Density): Float = with(density) { barCornerRadius.toPx() } + internal fun touchCircleRadiusPx(density: Density): Float = with(density) { touchCircleRadius.toPx() } + internal fun touchCircleShadowSizePx(density: Density): Float = with(density) { touchCircleShadowSize.toPx() } + internal fun touchCircleShadowTouchedSizeAdditionPx(density: Density): Float = with(density) { touchCircleShadowTouchedSizeAddition.toPx() } + internal fun stepMarkerRadiusPx(density: Density): Float = with(density) { (barHeight / 4).toPx() } + internal fun textSizePx(density: Density): Float = with(density) { textSize.toPx() } + + internal fun textInRangePaint(density: Density): Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = textColorInRange.toArgb() + this.textSize = textSizePx(density) + } + + internal fun textSelectedPaint(density: Density): Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = textColorInRange.toArgb() + this.textSize = textSizePx(density) + this.typeface = Typeface.DEFAULT_BOLD + } + + internal fun textOutOfRangePaint(density: Density): Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = textColorOutOfRange.toArgb() + this.textSize = textSizePx(density) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/laseroptek/raman/utils/slider/TouchInteractionModifier.kt b/app/src/main/java/com/laseroptek/raman/utils/slider/TouchInteractionModifier.kt new file mode 100644 index 0000000..f806f96 --- /dev/null +++ b/app/src/main/java/com/laseroptek/raman/utils/slider/TouchInteractionModifier.kt @@ -0,0 +1,34 @@ +package com.laseroptek.raman.utils.slider + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange + +sealed class TouchInteraction { + object NoInteraction : TouchInteraction() + object Up : TouchInteraction() + data class Move(val position: Offset) : TouchInteraction() +} + +fun Modifier.touchInteraction(key: Any, block: (TouchInteraction) -> Unit): Modifier = + pointerInput(key) { + awaitEachGesture { + do { + val event: PointerEvent = awaitPointerEvent() + + event.changes + .forEach { pointerInputChange: PointerInputChange -> + if (pointerInputChange.positionChange() != Offset.Zero) + pointerInputChange.consume() + } + + block(TouchInteraction.Move(event.changes.first().position)) + } while (event.changes.any { it.pressed }) + + block(TouchInteraction.Up) + } + } diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher_background.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher_background.xml new file mode 100644 index 0000000..f0b75b6 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher_background.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher_foreground.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher_foreground.xml new file mode 100644 index 0000000..94ba9c8 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher_foreground.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/background.png b/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..163e34f Binary files /dev/null and b/app/src/main/res/drawable/background.png differ diff --git a/app/src/main/res/drawable/ic_alert.png b/app/src/main/res/drawable/ic_alert.png new file mode 100644 index 0000000..74aac63 Binary files /dev/null and b/app/src/main/res/drawable/ic_alert.png differ diff --git a/app/src/main/res/drawable/ic_arrow_down.png b/app/src/main/res/drawable/ic_arrow_down.png new file mode 100644 index 0000000..de7b980 Binary files /dev/null and b/app/src/main/res/drawable/ic_arrow_down.png differ diff --git a/app/src/main/res/drawable/ic_arrow_left.png b/app/src/main/res/drawable/ic_arrow_left.png new file mode 100644 index 0000000..1ac0ded Binary files /dev/null and b/app/src/main/res/drawable/ic_arrow_left.png differ diff --git a/app/src/main/res/drawable/ic_arrow_right.png b/app/src/main/res/drawable/ic_arrow_right.png new file mode 100644 index 0000000..e763820 Binary files /dev/null and b/app/src/main/res/drawable/ic_arrow_right.png differ diff --git a/app/src/main/res/drawable/ic_arrow_up.png b/app/src/main/res/drawable/ic_arrow_up.png new file mode 100644 index 0000000..763cdd8 Binary files /dev/null and b/app/src/main/res/drawable/ic_arrow_up.png differ diff --git a/app/src/main/res/drawable/ic_btn_control.png b/app/src/main/res/drawable/ic_btn_control.png new file mode 100644 index 0000000..6f4e05c Binary files /dev/null and b/app/src/main/res/drawable/ic_btn_control.png differ diff --git a/app/src/main/res/drawable/ic_cancel_btn.png b/app/src/main/res/drawable/ic_cancel_btn.png new file mode 100644 index 0000000..a62f8d7 Binary files /dev/null and b/app/src/main/res/drawable/ic_cancel_btn.png differ diff --git a/app/src/main/res/drawable/ic_clock.png b/app/src/main/res/drawable/ic_clock.png new file mode 100644 index 0000000..8de6bfc Binary files /dev/null and b/app/src/main/res/drawable/ic_clock.png differ diff --git a/app/src/main/res/drawable/ic_close.png b/app/src/main/res/drawable/ic_close.png new file mode 100644 index 0000000..b9574a3 Binary files /dev/null and b/app/src/main/res/drawable/ic_close.png differ diff --git a/app/src/main/res/drawable/ic_completed.png b/app/src/main/res/drawable/ic_completed.png new file mode 100644 index 0000000..3f9eb27 Binary files /dev/null and b/app/src/main/res/drawable/ic_completed.png differ diff --git a/app/src/main/res/drawable/ic_control_duration.png b/app/src/main/res/drawable/ic_control_duration.png new file mode 100644 index 0000000..00cc220 Binary files /dev/null and b/app/src/main/res/drawable/ic_control_duration.png differ diff --git a/app/src/main/res/drawable/ic_control_fluence.png b/app/src/main/res/drawable/ic_control_fluence.png new file mode 100644 index 0000000..fe7cb70 Binary files /dev/null and b/app/src/main/res/drawable/ic_control_fluence.png differ diff --git a/app/src/main/res/drawable/ic_control_repetition.png b/app/src/main/res/drawable/ic_control_repetition.png new file mode 100644 index 0000000..a5c8816 Binary files /dev/null and b/app/src/main/res/drawable/ic_control_repetition.png differ diff --git a/app/src/main/res/drawable/ic_cooler.png b/app/src/main/res/drawable/ic_cooler.png new file mode 100644 index 0000000..3fc1da3 Binary files /dev/null and b/app/src/main/res/drawable/ic_cooler.png differ diff --git a/app/src/main/res/drawable/ic_cooler_2x.png b/app/src/main/res/drawable/ic_cooler_2x.png new file mode 100644 index 0000000..28e05dc Binary files /dev/null and b/app/src/main/res/drawable/ic_cooler_2x.png differ diff --git a/app/src/main/res/drawable/ic_dcd_setting2_btn.png b/app/src/main/res/drawable/ic_dcd_setting2_btn.png new file mode 100644 index 0000000..041a32a Binary files /dev/null and b/app/src/main/res/drawable/ic_dcd_setting2_btn.png differ diff --git a/app/src/main/res/drawable/ic_default.png b/app/src/main/res/drawable/ic_default.png new file mode 100644 index 0000000..5112450 Binary files /dev/null and b/app/src/main/res/drawable/ic_default.png differ diff --git a/app/src/main/res/drawable/ic_delete_btn.png b/app/src/main/res/drawable/ic_delete_btn.png new file mode 100644 index 0000000..4bb4503 Binary files /dev/null and b/app/src/main/res/drawable/ic_delete_btn.png differ diff --git a/app/src/main/res/drawable/ic_down.png b/app/src/main/res/drawable/ic_down.png new file mode 100644 index 0000000..45c023c Binary files /dev/null and b/app/src/main/res/drawable/ic_down.png differ diff --git a/app/src/main/res/drawable/ic_edit.png b/app/src/main/res/drawable/ic_edit.png new file mode 100644 index 0000000..40ed697 Binary files /dev/null and b/app/src/main/res/drawable/ic_edit.png differ diff --git a/app/src/main/res/drawable/ic_edit_btn.png b/app/src/main/res/drawable/ic_edit_btn.png new file mode 100644 index 0000000..ad23728 Binary files /dev/null and b/app/src/main/res/drawable/ic_edit_btn.png differ diff --git a/app/src/main/res/drawable/ic_empty.png b/app/src/main/res/drawable/ic_empty.png new file mode 100644 index 0000000..4c36ea5 Binary files /dev/null and b/app/src/main/res/drawable/ic_empty.png differ diff --git a/app/src/main/res/drawable/ic_error.png b/app/src/main/res/drawable/ic_error.png new file mode 100644 index 0000000..3015614 Binary files /dev/null and b/app/src/main/res/drawable/ic_error.png differ diff --git a/app/src/main/res/drawable/ic_export.png b/app/src/main/res/drawable/ic_export.png new file mode 100644 index 0000000..904d4c9 Binary files /dev/null and b/app/src/main/res/drawable/ic_export.png differ diff --git a/app/src/main/res/drawable/ic_failed.png b/app/src/main/res/drawable/ic_failed.png new file mode 100644 index 0000000..c0d4d26 Binary files /dev/null and b/app/src/main/res/drawable/ic_failed.png differ diff --git a/app/src/main/res/drawable/ic_full.png b/app/src/main/res/drawable/ic_full.png new file mode 100644 index 0000000..089c058 Binary files /dev/null and b/app/src/main/res/drawable/ic_full.png differ diff --git a/app/src/main/res/drawable/ic_globe.png b/app/src/main/res/drawable/ic_globe.png new file mode 100644 index 0000000..904a2c0 Binary files /dev/null and b/app/src/main/res/drawable/ic_globe.png differ diff --git a/app/src/main/res/drawable/ic_heart.png b/app/src/main/res/drawable/ic_heart.png new file mode 100644 index 0000000..149ddf1 Binary files /dev/null and b/app/src/main/res/drawable/ic_heart.png differ diff --git a/app/src/main/res/drawable/ic_home.png b/app/src/main/res/drawable/ic_home.png new file mode 100644 index 0000000..7e0251f Binary files /dev/null and b/app/src/main/res/drawable/ic_home.png differ diff --git a/app/src/main/res/drawable/ic_import.png b/app/src/main/res/drawable/ic_import.png new file mode 100644 index 0000000..f22eb22 Binary files /dev/null and b/app/src/main/res/drawable/ic_import.png differ diff --git a/app/src/main/res/drawable/ic_info.png b/app/src/main/res/drawable/ic_info.png new file mode 100644 index 0000000..d0c54bd Binary files /dev/null and b/app/src/main/res/drawable/ic_info.png differ diff --git a/app/src/main/res/drawable/ic_laseron.png b/app/src/main/res/drawable/ic_laseron.png new file mode 100644 index 0000000..ea9c256 Binary files /dev/null and b/app/src/main/res/drawable/ic_laseron.png differ diff --git a/app/src/main/res/drawable/ic_left.png b/app/src/main/res/drawable/ic_left.png new file mode 100644 index 0000000..871fdd8 Binary files /dev/null and b/app/src/main/res/drawable/ic_left.png differ diff --git a/app/src/main/res/drawable/ic_left_yellow.png b/app/src/main/res/drawable/ic_left_yellow.png new file mode 100644 index 0000000..eac3a17 Binary files /dev/null and b/app/src/main/res/drawable/ic_left_yellow.png differ diff --git a/app/src/main/res/drawable/ic_load.png b/app/src/main/res/drawable/ic_load.png new file mode 100644 index 0000000..218b1de Binary files /dev/null and b/app/src/main/res/drawable/ic_load.png differ diff --git a/app/src/main/res/drawable/ic_load_btn.png b/app/src/main/res/drawable/ic_load_btn.png new file mode 100644 index 0000000..47dc5df Binary files /dev/null and b/app/src/main/res/drawable/ic_load_btn.png differ diff --git a/app/src/main/res/drawable/ic_lock.png b/app/src/main/res/drawable/ic_lock.png new file mode 100644 index 0000000..5adaa21 Binary files /dev/null and b/app/src/main/res/drawable/ic_lock.png differ diff --git a/app/src/main/res/drawable/ic_lock_screen.png b/app/src/main/res/drawable/ic_lock_screen.png new file mode 100644 index 0000000..9e3f074 Binary files /dev/null and b/app/src/main/res/drawable/ic_lock_screen.png differ diff --git a/app/src/main/res/drawable/ic_lock_small.png b/app/src/main/res/drawable/ic_lock_small.png new file mode 100644 index 0000000..badd0fd Binary files /dev/null and b/app/src/main/res/drawable/ic_lock_small.png differ diff --git a/app/src/main/res/drawable/ic_log.png b/app/src/main/res/drawable/ic_log.png new file mode 100644 index 0000000..f6c2330 Binary files /dev/null and b/app/src/main/res/drawable/ic_log.png differ diff --git a/app/src/main/res/drawable/ic_play.png b/app/src/main/res/drawable/ic_play.png new file mode 100644 index 0000000..d28e400 Binary files /dev/null and b/app/src/main/res/drawable/ic_play.png differ diff --git a/app/src/main/res/drawable/ic_preset_delete2.png b/app/src/main/res/drawable/ic_preset_delete2.png new file mode 100644 index 0000000..ce50cf6 Binary files /dev/null and b/app/src/main/res/drawable/ic_preset_delete2.png differ diff --git a/app/src/main/res/drawable/ic_preset_edit2.png b/app/src/main/res/drawable/ic_preset_edit2.png new file mode 100644 index 0000000..a2266d5 Binary files /dev/null and b/app/src/main/res/drawable/ic_preset_edit2.png differ diff --git a/app/src/main/res/drawable/ic_preset_load.png b/app/src/main/res/drawable/ic_preset_load.png new file mode 100644 index 0000000..32a25b4 Binary files /dev/null and b/app/src/main/res/drawable/ic_preset_load.png differ diff --git a/app/src/main/res/drawable/ic_preset_save.png b/app/src/main/res/drawable/ic_preset_save.png new file mode 100644 index 0000000..88cc0cd Binary files /dev/null and b/app/src/main/res/drawable/ic_preset_save.png differ diff --git a/app/src/main/res/drawable/ic_purge.png b/app/src/main/res/drawable/ic_purge.png new file mode 100644 index 0000000..ec1bd6a Binary files /dev/null and b/app/src/main/res/drawable/ic_purge.png differ diff --git a/app/src/main/res/drawable/ic_purge3.png b/app/src/main/res/drawable/ic_purge3.png new file mode 100644 index 0000000..f68bcc8 Binary files /dev/null and b/app/src/main/res/drawable/ic_purge3.png differ diff --git a/app/src/main/res/drawable/ic_purge_btn.png b/app/src/main/res/drawable/ic_purge_btn.png new file mode 100644 index 0000000..bf31663 Binary files /dev/null and b/app/src/main/res/drawable/ic_purge_btn.png differ diff --git a/app/src/main/res/drawable/ic_purge_off.png b/app/src/main/res/drawable/ic_purge_off.png new file mode 100644 index 0000000..c8ec3ee Binary files /dev/null and b/app/src/main/res/drawable/ic_purge_off.png differ diff --git a/app/src/main/res/drawable/ic_ready.png b/app/src/main/res/drawable/ic_ready.png new file mode 100644 index 0000000..67b7578 Binary files /dev/null and b/app/src/main/res/drawable/ic_ready.png differ diff --git a/app/src/main/res/drawable/ic_refresh.png b/app/src/main/res/drawable/ic_refresh.png new file mode 100644 index 0000000..6aa2f53 Binary files /dev/null and b/app/src/main/res/drawable/ic_refresh.png differ diff --git a/app/src/main/res/drawable/ic_reload.png b/app/src/main/res/drawable/ic_reload.png new file mode 100644 index 0000000..9e8ce32 Binary files /dev/null and b/app/src/main/res/drawable/ic_reload.png differ diff --git a/app/src/main/res/drawable/ic_right.png b/app/src/main/res/drawable/ic_right.png new file mode 100644 index 0000000..2c7cf25 Binary files /dev/null and b/app/src/main/res/drawable/ic_right.png differ diff --git a/app/src/main/res/drawable/ic_right_2.png b/app/src/main/res/drawable/ic_right_2.png new file mode 100644 index 0000000..b35b50e Binary files /dev/null and b/app/src/main/res/drawable/ic_right_2.png differ diff --git a/app/src/main/res/drawable/ic_right_yellow.png b/app/src/main/res/drawable/ic_right_yellow.png new file mode 100644 index 0000000..89668a6 Binary files /dev/null and b/app/src/main/res/drawable/ic_right_yellow.png differ diff --git a/app/src/main/res/drawable/ic_round_check.png b/app/src/main/res/drawable/ic_round_check.png new file mode 100644 index 0000000..4e9cd71 Binary files /dev/null and b/app/src/main/res/drawable/ic_round_check.png differ diff --git a/app/src/main/res/drawable/ic_save.png b/app/src/main/res/drawable/ic_save.png new file mode 100644 index 0000000..339cde6 Binary files /dev/null and b/app/src/main/res/drawable/ic_save.png differ diff --git a/app/src/main/res/drawable/ic_save_btn.png b/app/src/main/res/drawable/ic_save_btn.png new file mode 100644 index 0000000..9a89803 Binary files /dev/null and b/app/src/main/res/drawable/ic_save_btn.png differ diff --git a/app/src/main/res/drawable/ic_setting.png b/app/src/main/res/drawable/ic_setting.png new file mode 100644 index 0000000..a63e15c Binary files /dev/null and b/app/src/main/res/drawable/ic_setting.png differ diff --git a/app/src/main/res/drawable/ic_speaker_off.png b/app/src/main/res/drawable/ic_speaker_off.png new file mode 100644 index 0000000..5de54f0 Binary files /dev/null and b/app/src/main/res/drawable/ic_speaker_off.png differ diff --git a/app/src/main/res/drawable/ic_speaker_on.png b/app/src/main/res/drawable/ic_speaker_on.png new file mode 100644 index 0000000..b67741c Binary files /dev/null and b/app/src/main/res/drawable/ic_speaker_on.png differ diff --git a/app/src/main/res/drawable/ic_spray.png b/app/src/main/res/drawable/ic_spray.png new file mode 100644 index 0000000..e0b1c66 Binary files /dev/null and b/app/src/main/res/drawable/ic_spray.png differ diff --git a/app/src/main/res/drawable/ic_standby.png b/app/src/main/res/drawable/ic_standby.png new file mode 100644 index 0000000..bdbb890 Binary files /dev/null and b/app/src/main/res/drawable/ic_standby.png differ diff --git a/app/src/main/res/drawable/ic_test.png b/app/src/main/res/drawable/ic_test.png new file mode 100644 index 0000000..2ecb636 Binary files /dev/null and b/app/src/main/res/drawable/ic_test.png differ diff --git a/app/src/main/res/drawable/ic_text_settings.xml b/app/src/main/res/drawable/ic_text_settings.xml new file mode 100644 index 0000000..1158761 --- /dev/null +++ b/app/src/main/res/drawable/ic_text_settings.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_unlock.png b/app/src/main/res/drawable/ic_unlock.png new file mode 100644 index 0000000..6ff5d78 Binary files /dev/null and b/app/src/main/res/drawable/ic_unlock.png differ diff --git a/app/src/main/res/drawable/ic_up.png b/app/src/main/res/drawable/ic_up.png new file mode 100644 index 0000000..0dc9aa6 Binary files /dev/null and b/app/src/main/res/drawable/ic_up.png differ diff --git a/app/src/main/res/drawable/ic_update.png b/app/src/main/res/drawable/ic_update.png new file mode 100644 index 0000000..5479acb Binary files /dev/null and b/app/src/main/res/drawable/ic_update.png differ diff --git a/app/src/main/res/drawable/ic_valve.png b/app/src/main/res/drawable/ic_valve.png new file mode 100644 index 0000000..25f6f55 Binary files /dev/null and b/app/src/main/res/drawable/ic_valve.png differ diff --git a/app/src/main/res/drawable/ic_warning.png b/app/src/main/res/drawable/ic_warning.png new file mode 100644 index 0000000..9658270 Binary files /dev/null and b/app/src/main/res/drawable/ic_warning.png differ diff --git a/app/src/main/res/drawable/ic_x.png b/app/src/main/res/drawable/ic_x.png new file mode 100644 index 0000000..9846fef Binary files /dev/null and b/app/src/main/res/drawable/ic_x.png differ diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..07ece49 Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ diff --git a/app/src/main/res/drawable/splash_background.png b/app/src/main/res/drawable/splash_background.png new file mode 100644 index 0000000..e0f3671 Binary files /dev/null and b/app/src/main/res/drawable/splash_background.png differ diff --git a/app/src/main/res/font/montserrat_bold.ttf b/app/src/main/res/font/montserrat_bold.ttf new file mode 100644 index 0000000..0927b81 Binary files /dev/null and b/app/src/main/res/font/montserrat_bold.ttf differ diff --git a/app/src/main/res/font/montserrat_medium.ttf b/app/src/main/res/font/montserrat_medium.ttf new file mode 100644 index 0000000..6e079f6 Binary files /dev/null and b/app/src/main/res/font/montserrat_medium.ttf differ diff --git a/app/src/main/res/font/montserrat_regular.ttf b/app/src/main/res/font/montserrat_regular.ttf new file mode 100644 index 0000000..8d443d5 Binary files /dev/null and b/app/src/main/res/font/montserrat_regular.ttf differ diff --git a/app/src/main/res/font/pretendard_black.otf b/app/src/main/res/font/pretendard_black.otf new file mode 100644 index 0000000..04cc09d Binary files /dev/null and b/app/src/main/res/font/pretendard_black.otf differ diff --git a/app/src/main/res/font/pretendard_bold.otf b/app/src/main/res/font/pretendard_bold.otf new file mode 100644 index 0000000..a52ef39 Binary files /dev/null and b/app/src/main/res/font/pretendard_bold.otf differ diff --git a/app/src/main/res/font/pretendard_extrabold.otf b/app/src/main/res/font/pretendard_extrabold.otf new file mode 100644 index 0000000..33d4371 Binary files /dev/null and b/app/src/main/res/font/pretendard_extrabold.otf differ diff --git a/app/src/main/res/font/pretendard_extralight.otf b/app/src/main/res/font/pretendard_extralight.otf new file mode 100644 index 0000000..8952156 Binary files /dev/null and b/app/src/main/res/font/pretendard_extralight.otf differ diff --git a/app/src/main/res/font/pretendard_light.otf b/app/src/main/res/font/pretendard_light.otf new file mode 100644 index 0000000..fefa785 Binary files /dev/null and b/app/src/main/res/font/pretendard_light.otf differ diff --git a/app/src/main/res/font/pretendard_medium.otf b/app/src/main/res/font/pretendard_medium.otf new file mode 100644 index 0000000..a2dc009 Binary files /dev/null and b/app/src/main/res/font/pretendard_medium.otf differ diff --git a/app/src/main/res/font/pretendard_regular.otf b/app/src/main/res/font/pretendard_regular.otf new file mode 100644 index 0000000..c940185 Binary files /dev/null and b/app/src/main/res/font/pretendard_regular.otf differ diff --git a/app/src/main/res/font/pretendard_semibold.otf b/app/src/main/res/font/pretendard_semibold.otf new file mode 100644 index 0000000..c375b54 Binary files /dev/null and b/app/src/main/res/font/pretendard_semibold.otf differ diff --git a/app/src/main/res/font/pretendard_thin.otf b/app/src/main/res/font/pretendard_thin.otf new file mode 100644 index 0000000..a8c24dd Binary files /dev/null and b/app/src/main/res/font/pretendard_thin.otf differ diff --git a/app/src/main/res/font/racingsansone_regular.ttf b/app/src/main/res/font/racingsansone_regular.ttf new file mode 100644 index 0000000..64d5b75 Binary files /dev/null and b/app/src/main/res/font/racingsansone_regular.ttf differ diff --git a/app/src/main/res/font/roboto_black.ttf b/app/src/main/res/font/roboto_black.ttf new file mode 100644 index 0000000..58fa175 Binary files /dev/null and b/app/src/main/res/font/roboto_black.ttf differ diff --git a/app/src/main/res/font/roboto_blackitalic.ttf b/app/src/main/res/font/roboto_blackitalic.ttf new file mode 100644 index 0000000..0a4dfd0 Binary files /dev/null and b/app/src/main/res/font/roboto_blackitalic.ttf differ diff --git a/app/src/main/res/font/roboto_bold.ttf b/app/src/main/res/font/roboto_bold.ttf new file mode 100644 index 0000000..e64db79 Binary files /dev/null and b/app/src/main/res/font/roboto_bold.ttf differ diff --git a/app/src/main/res/font/roboto_bolditalic.ttf b/app/src/main/res/font/roboto_bolditalic.ttf new file mode 100644 index 0000000..5e39ae9 Binary files /dev/null and b/app/src/main/res/font/roboto_bolditalic.ttf differ diff --git a/app/src/main/res/font/roboto_italic.ttf b/app/src/main/res/font/roboto_italic.ttf new file mode 100644 index 0000000..65498ee Binary files /dev/null and b/app/src/main/res/font/roboto_italic.ttf differ diff --git a/app/src/main/res/font/roboto_light.ttf b/app/src/main/res/font/roboto_light.ttf new file mode 100644 index 0000000..a7e0284 Binary files /dev/null and b/app/src/main/res/font/roboto_light.ttf differ diff --git a/app/src/main/res/font/roboto_lightitalic.ttf b/app/src/main/res/font/roboto_lightitalic.ttf new file mode 100644 index 0000000..867b76d Binary files /dev/null and b/app/src/main/res/font/roboto_lightitalic.ttf differ diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000..0707e15 Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/font/roboto_mediumitalic.ttf b/app/src/main/res/font/roboto_mediumitalic.ttf new file mode 100644 index 0000000..4e3bf0d Binary files /dev/null and b/app/src/main/res/font/roboto_mediumitalic.ttf differ diff --git a/app/src/main/res/font/roboto_mono_bold.ttf b/app/src/main/res/font/roboto_mono_bold.ttf new file mode 100644 index 0000000..07ef607 Binary files /dev/null and b/app/src/main/res/font/roboto_mono_bold.ttf differ diff --git a/app/src/main/res/font/roboto_mono_bold_italic.ttf b/app/src/main/res/font/roboto_mono_bold_italic.ttf new file mode 100644 index 0000000..1cca0bf Binary files /dev/null and b/app/src/main/res/font/roboto_mono_bold_italic.ttf differ diff --git a/app/src/main/res/font/roboto_mono_italic.ttf b/app/src/main/res/font/roboto_mono_italic.ttf new file mode 100644 index 0000000..ef92c37 Binary files /dev/null and b/app/src/main/res/font/roboto_mono_italic.ttf differ diff --git a/app/src/main/res/font/roboto_mono_light.ttf b/app/src/main/res/font/roboto_mono_light.ttf new file mode 100644 index 0000000..63229b2 Binary files /dev/null and b/app/src/main/res/font/roboto_mono_light.ttf differ diff --git a/app/src/main/res/font/roboto_mono_light_italic.ttf b/app/src/main/res/font/roboto_mono_light_italic.ttf new file mode 100644 index 0000000..f25bed5 Binary files /dev/null and b/app/src/main/res/font/roboto_mono_light_italic.ttf differ diff --git a/app/src/main/res/font/roboto_mono_medium.ttf b/app/src/main/res/font/roboto_mono_medium.ttf new file mode 100644 index 0000000..88ff0c1 Binary files /dev/null and b/app/src/main/res/font/roboto_mono_medium.ttf differ diff --git a/app/src/main/res/font/roboto_mono_medium_italic.ttf b/app/src/main/res/font/roboto_mono_medium_italic.ttf new file mode 100644 index 0000000..307efad Binary files /dev/null and b/app/src/main/res/font/roboto_mono_medium_italic.ttf differ diff --git a/app/src/main/res/font/roboto_mono_regular.ttf b/app/src/main/res/font/roboto_mono_regular.ttf new file mode 100644 index 0000000..b158a33 Binary files /dev/null and b/app/src/main/res/font/roboto_mono_regular.ttf differ diff --git a/app/src/main/res/font/roboto_mono_thin.ttf b/app/src/main/res/font/roboto_mono_thin.ttf new file mode 100644 index 0000000..309484d Binary files /dev/null and b/app/src/main/res/font/roboto_mono_thin.ttf differ diff --git a/app/src/main/res/font/roboto_mono_thin_italic.ttf b/app/src/main/res/font/roboto_mono_thin_italic.ttf new file mode 100644 index 0000000..e1bb912 Binary files /dev/null and b/app/src/main/res/font/roboto_mono_thin_italic.ttf differ diff --git a/app/src/main/res/font/roboto_regular.ttf b/app/src/main/res/font/roboto_regular.ttf new file mode 100644 index 0000000..2d116d9 Binary files /dev/null and b/app/src/main/res/font/roboto_regular.ttf differ diff --git a/app/src/main/res/font/roboto_thin.ttf b/app/src/main/res/font/roboto_thin.ttf new file mode 100644 index 0000000..ab68508 Binary files /dev/null and b/app/src/main/res/font/roboto_thin.ttf differ diff --git a/app/src/main/res/font/roboto_thinitalic.ttf b/app/src/main/res/font/roboto_thinitalic.ttf new file mode 100644 index 0000000..b2c3933 Binary files /dev/null and b/app/src/main/res/font/roboto_thinitalic.ttf differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c935046 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..0689207 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..41189c3 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..9934af1 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..4fc37c0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..645a6e3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/raw/sound.wav b/app/src/main/res/raw/sound.wav new file mode 100644 index 0000000..7422460 Binary files /dev/null and b/app/src/main/res/raw/sound.wav differ diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml new file mode 100644 index 0000000..5e422d4 --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,180 @@ + + + VasCURA589 + + Info + Config + Home + Engineer + + Set a password for device lock + Info + Lock + Enter your transaction pin to continue + Can\'t update latest news + Retry + Add to favorites + Close + Start + + + + + Engineer mode permission password + Engineer Mode + OS Setup + Update + Default + Save + Load + Import + Export + Log + Data + SET + Serial Number + Product + Laser Hand + Power Supply + Program Version + Repetition(Hz) + Voltage(V) + Pulse-Width(ms) + DCD + CAN Type + DCD Lifespan Adjust + Lamp Total Count + Device Operation Time + + Q Switch Value + + H/P + + Energy Detect - Refer 1. + Energy Detect - Refer 2. + + Lamp Life Time + HP5x5 Life Time + HP7x7 Life Time + HP10x10 Life Time + HP12x12 Life Time + HP3x15 Life Time + Detector Life Time + Water Life Time + + + KTP Value + Chamber1 Value + Chamber2 Value + Water Value + Baseplate Value + + KTP Temperature Value + Chamber 1 Temperature Value + Chamber 2 Temperature Value + Water Temperature Value + Baseplate Temperature Value + + Do you want to update app ? + Do you want to load default table ? + Do you want to save current table ? + Do you want to set current values ? + Do you want to load saved table ? + Do you want to export saved table to external storage ? + Do you want to import table from external storage ? + + All + Laser Status + Temperature + Alarm + + + DATE + TIME + English + Korean + + + Error + Warning + Energy Check + + Home + OK + + Loading + Cancel + Save + STANDBY + READY + LASER ON + Flash Lamp Total Count + Software Info + Environment Monitoring + System Monitoring + Current Internal\nEnvironment + Current External\nEnvironment + Current System\nTemperature + + Pulse Type + Single + Repeat + Guide Beam + Volume + Time Setting + Language + + + Do you want to reset the laser count ? + Cooling + LASER COUNT + Spray + Delay + Do you want to reset the DCD count ? + Do you want to delete the selected row ? + + StandBy + Back to STANDBY + PULSE DURATION + FLUENCE + REPETITION + Use the laser by foot switch + In an emergency, use the emergency switch on the device + Software Information + None + P/S COMM. + HEAD COMM. + Simmer + Charge + Discharge + Optical System + Water Level + Water Flow + E. Detect system + KTP Temp. + Chamber Temp. + Baseplate Temp. + Water Temp. + DCD Temp + DCD Pressure + Shutter1 + Shutter2 + Not Defined + Interlock + Other + None + Water Level + Optical Energy + DCD GAS shortage + DCD bubble + Internal Temp High + Internal Temp Low + Humidity #1 + Humidity #2 + HP Tip + Lamp Count + Not Defined + + UNLOCK + Shots + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000..75c4bab --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,179 @@ + + + VasCURA589 + + Info + Config + Home + Engineer + + Set a password for device lock + Info + Lock + Enter your transaction pin to continue + Can\'t update latest news + Retry + Add to favorites + Close + Start + + + + + Engineer mode permission password + Engineer Mode + OS Setup + Update + Default + Save + Load + Import + Export + Log + Data + SET + Serial Number + Product + Laser Hand + Power Supply + Program Version + Repetition(Hz) + Voltage(V) + Pulse-Width(ms) + DCD + CAN Type + DCD Lifespan Adjust + Lamp Total Count + Device Operation Time + Q Switch Value + + H/P + + Energy Detect - Refer 1. + Energy Detect - Refer 2. + + Lamp Life Time + HP5x5 Life Time + HP7x7 Life Time + HP10x10 Life Time + HP12x12 Life Time + HP3x15 Life Time + Detector Life Time + Water Life Time + + KTP Count Value + Chamber1 Value + Chamber2 Value + Water Value + Baseplate Value + + KTP Temperature Value + Chamber 1 Temperature Value + Chamber 2 Temperature Value + Water Temperature Value + Baseplate Temperature Value + + Do you want to update app ? + Do you want to load default table ? + Do you want to save current table ? + Do you want to set current values ? + Do you want to load saved table ? + Do you want to export saved table to external storage ? + Do you want to import table from external storage ? + + All + Laser Status + Temperature + Alarm + + + 시간 + 날짜 + 영어 + 한글 + + + Error + Warning + Energy Check + + Home + OK + + Loading + Cancel + Save + STANDBY + READY + LASER ON + Flash Lamp Total Count + Software Info + Environment Monitoring + System Monitoring + Current Internal\nEnvironment + Current External\nEnvironment + Current System\nTemperature + + Pulse Type + Single + Repeat + Guide Beam + Volume + Time Setting + Language + + + Do you want to reset the laser count ? + Cooling + LASER COUNT + Spray + Delay + Do you want to reset the DCD count ? + Do you want to delete the selected row ? + + StandBy + Back to STANDBY + PULSE DURATION + FLUENCE + REPETITION + Use the laser by foot switch + In an emergency, use the emergency switch on the device + Software Information + + None + P/S COMM. + HEAD COMM. + Simmer + Charge + Discharge + Optical System + Water Level + Water Flow + E. Detect system + KTP Temp. + Chamber Temp. + Baseplate Temp. + Water Temp. + DCD Temp + DCD Pressure + Shutter1 + Shutter2 + Not Defined + Interlock + Other + None + Water Level + Optical Energy + DCD GAS shortage + DCD bubble + Internal Temp High + Internal Temp Low + Humidity #1 + Humidity #2 + HP Tip + Lamp Count + Not Defined + + UNLOCK + Shots + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..513dd6a --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 0000000..b54e03f --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..2229e2d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,195 @@ + + + + VasCURA589 + + + Home + Info + Config + Lock + Engineer + + Set a password for device lock + + Can\'t update latest news + Add to favorites + Share + Text settings + unbookmark + bookmark + Close + Start + + + + + Engineer mode permission password + Engineer Mode + OS Setup + Update + Default + Save + Load + Import + Export + Log + Data + SET + Serial Number + Product + Laser Hand + Power Supply + Program Version + Repetition(Hz) + Voltage(V) + Pulse-Width(ms) + DCD + CAN Type + DCD Lifespan Adjust + Lamp Total Count + Device Operation Time + Q Switch Value + + H/P + + Energy Detect - Refer 1. + Energy Detect - Refer 2. + + Lamp Life Time + HP5x5 Life Time + HP7x7 Life Time + HP10x10 Life Time + HP12x12 Life Time + HP3x15 Life Time + Detector Life Time + Water Life Time + + + KTP Value + Chamber1 Value + Chamber2 Value + Water Value + Baseplate Value + + KTP Temperature Value + Chamber 1 Temperature Value + Chamber 2 Temperature Value + Water Temperature Value + Baseplate Temperature Value + + Do you want to update app ? + Do you want to load default table ? + Do you want to save current table ? + Do you want to set current values ? + Do you want to load saved table ? + Do you want to export saved table to external storage ? + Do you want to import table from external storage ? + + All + Laser Status + Temperature + Alarm + + + DATE + TIME + English + Korean + + + Error + Warning + Energy Check + + OK + + Loading + Cancel + Save + STANDBY + READY + LASER ON + Flash Lamp Total Count + Software Info + Environment Monitoring + System Monitoring + Current Internal\nEnvironment + Current External\nEnvironment + Current System\nTemperature + + Pulse Type + Single + Repeat + Guide Beam + Volume + Time Setting + Language + + + Do you want to reset the laser count ? + Cooling + LASER COUNT + Spray + Delay + Do you want to reset the DCD count ? + Do you want to delete the selected row ? + + StandBy + Back to STANDBY + PULSE DURATION + FLUENCE + REPETITION + Use the laser by foot switch + In an emergency, use the emergency switch on the device + Software Information + None + P/S COMM. + HEAD COMM. + Simmer + Charge + Discharge + Optical System + Water Level + Water Flow + E. Detect system + KTP Temp. + Chamber Temp. + Baseplate Temp. + Water Temp. + DCD Temp + DCD Pressure + Shutter1 + Shutter2 + Not Defined + Interlock + Other + None + Water Level + Optical Energy + DCD GAS shortage + DCD bubble + Internal Temp High + Internal Temp Low + Humidity #1 + Humidity #2 + HP Tip + Lamp Count + Not Defined + + UNLOCK + Shots + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..0d1dff2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,25 @@ + + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4d577dc --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +plugins { + alias(libs.plugins.gradle.versions) + alias(libs.plugins.version.catalog.update) + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.compose) apply false + +} + +//apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") diff --git a/buildscripts/init.gradle.kts b/buildscripts/init.gradle.kts new file mode 100644 index 0000000..1b7a542 --- /dev/null +++ b/buildscripts/init.gradle.kts @@ -0,0 +1,72 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * 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. + */ + +val ktlintVersion = "0.46.1" + +initscript { + val spotlessVersion = "6.10.0" + + repositories { + mavenCentral() + } + + dependencies { + classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") + } +} + +allprojects { + if (this == rootProject) { + return@allprojects + } + apply() + extensions.configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(ktlintVersion).editorConfigOverride( + mapOf( + "ktlint_code_style" to "android", + "ij_kotlin_allow_trailing_comma" to true, + // These rules were introduced in ktlint 0.46.0 and should not be + // enabled without further discussion. They are disabled for now. + // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 + "disabled_rules" to + "filename," + + "annotation,annotation-spacing," + + "argument-list-wrapping," + + "double-colon-spacing," + + "enum-entry-name-case," + + "multiline-if-else," + + "no-empty-first-line-in-method-block," + + "package-name," + + "trailing-comma," + + "spacing-around-angle-brackets," + + "spacing-between-declarations-with-annotations," + + "spacing-between-declarations-with-comments," + + "unary-op-spacing" + ) + ) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + format("kts") { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} \ No newline at end of file diff --git a/buildscripts/toml-updater-config.gradle b/buildscripts/toml-updater-config.gradle new file mode 100644 index 0000000..2934311 --- /dev/null +++ b/buildscripts/toml-updater-config.gradle @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * 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. + */ +versionCatalogUpdate { + sortByKey.set(true) + + keep { + // keep versions without any library or plugin reference + keepUnusedVersions.set(true) + // keep all libraries that aren't used in the project + keepUnusedLibraries.set(true) + // keep all plugins that aren't used in the project + keepUnusedPlugins.set(true) + } +} + +def isNonStable = { String version -> + def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.toUpperCase().contains(it) } + def regex = /^[0-9,.v-]+(-r)?$/ + return !stableKeyword && !(version ==~ regex) +} + +tasks.named("dependencyUpdates").configure { + resolutionStrategy { + componentSelection { + all { + if (isNonStable(it.candidate.version) && !isNonStable(it.currentVersion)) { + reject('Release candidate') + } + } + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1b0d2f1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,48 @@ +# +# Copyright 2020 The Android Open Source Project +# +# 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 +# +# http://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. +# + +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m + +# Turn on parallel compilation, caching and on-demand configuration +org.gradle.configureondemand=true +org.gradle.caching=true +org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +# Enable R8 full mode. +android.enableR8.fullMode=true + +# BuildConfig (deprecated) +# android.defaults.buildfeatures.buildconfig=true + +# Version control +android.defaults.buildfeatures.buildconfig=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..fbd143b --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,206 @@ +##### +# This file is duplicated to individual samples from the global scripts/libs.versions.toml +# Do not add a dependency to an individual sample, edit the global version instead. +##### +[versions] +accompanist = "0.36.0" +androidGradlePlugin = "8.8.1" +androidx-activity-compose = "1.9.0" +androidx-appcompat = "1.7.1" +androidx-benchmark = "1.2.4" +androidx-benchmark-junit4 = "1.2.4" +androidx-compose-bom = "2024.06.00" +androidx-constraintlayout = "1.1.1" +androidx-core-splashscreen = "1.0.1" +androidx-corektx = "1.13.1" +androidx-glance = "1.1.1" +androidx-lifecycle = "2.8.2" +androidx-lifecycle-compose = "2.10.0" +androidx-lifecycle-runtime-compose = "2.8.0" +androidx-navigation = "2.7.7" +androidx-palette = "1.0.0" +androidx-test = "1.7.0" +androidx-test-espresso = "3.7.0" +androidx-test-ext-junit = "1.3.0" +androidx-test-ext-truth = "1.6.0" +androidx-tv-foundation = "1.0.0-alpha11" +androidx-tv-material = "1.0.0" +androidx-wear-compose = "1.3.1" +androidx-window = "1.5.0" +androidxHiltNavigationCompose = "1.3.0" +androix-test-uiautomator = "2.3.0" +coil = "2.6.0" +# @keep +compileSdk = "35" +composeBom = "2025.10.01" +coroutines = "1.8.0" +google-maps = "19.2.0" +gradle-versions = "0.52.0" +gson = "2.13.2" +hilt = "2.57.2" +hiltExt = "1.2.0" +horologist = "0.7.15" +# @pin When updating to AGP 7.4.0-alpha10 and up we can update this https://developer.android.com/studio/write/java8-support#library-desugaring-versions +jdkDesugar = "2.1.5" +junit = "4.13.2" +kotlin = "2.0.0" +kotlinx-serialization-json = "1.7.1" +kotlinx_immutable = "0.3.7" +kotlinxDatetime = "0.6.0" +ksp = "2.0.0-1.0.21" +lifecycleRuntimeKtxVersion = "2.8.0" +localbroadcastmanager = "1.1.0" +maps-compose = "6.7.1" +# @keep +okhttp = "5.1.0" +play-services-wearable = "19.0.0" +robolectric = "4.15.1" +roborazzi = "1.47.0" +rome = "2.1.0" +room = "2.8.2" +secrets = "2.0.1" +snapper = "0.3.0" +version-catalog-update = "1.0.0" +timber = "5.0.1" +lottieCompose = "6.6.10" +securityCryptoKtx = "1.1.0" +kmpDateTimePicker = "1.1.1" +runtime = "1.3.0" +# @keep +minSdk = "26" +maxSdk = "35" +#noinspection ExpiredTargetSdkVersion +targetSdk = "31" + +[libraries] +accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } +accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } +accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } +accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } +androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity-compose" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-benchmark-macrobenchmark = { module = "androidx.benchmark:benchmark-macro", version.ref = "androidx-benchmark" } +androidx-benchmark-macrobenchmark-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark-junit4" } +androidx-compose-animation = { module = "androidx.compose.animation:animation" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-bom-v20251001 = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } +androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive" } +androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout" } +androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation" } +androidx-compose-material3-adaptive-navigationSuite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" } +androidx-compose-materialWindow = { module = "androidx.compose.material3:material3-window-size-class" } +androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } +androidx-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } +androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" } +androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" } +androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "androidx-constraintlayout" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-corektx" } +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" } +androidx-glance = { module = "androidx.glance:glance", version.ref = "androidx-glance" } +androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "androidx-glance" } +androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "androidx-glance" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } +androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle-compose" } +androidx-localbroadcastmanager = { module = "androidx.localbroadcastmanager:localbroadcastmanager", version.ref = "localbroadcastmanager" } +androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } +androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" } +androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } +androidx-test-ext-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-ext-truth" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } +androidx-test-runner = "androidx.test:runner:1.7.0" +androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androix-test-uiautomator" } +androidx-tv-foundation = { module = "androidx.tv:tv-foundation", version.ref = "androidx-tv-foundation" } +androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "androidx-tv-material" } +androidx-wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "androidx-wear-compose" } +androidx-wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "androidx-wear-compose" } +androidx-wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "androidx-wear-compose" } +androidx-wear-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "androidx-wear-compose" } +androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } +androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window" } +coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +core-jdk-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "jdkDesugar" } +dagger-hiltandroidplugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } +googlemaps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" } +googlemaps-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "google-maps" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +hilt-ext-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltExt" } +horologist-audio-ui = { module = "com.google.android.horologist:horologist-audio-ui", version.ref = "horologist" } +horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" } +horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } +horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" } +horologist-compose-tools = { module = "com.google.android.horologist:horologist-compose-tools", version.ref = "horologist" } +horologist-images-coil = { module = "com.google.android.horologist:horologist-images-coil", version.ref = "horologist" } +horologist-media-data = { module = "com.google.android.horologist:horologist-media-data", version.ref = "horologist" } +horologist-media-ui = { module = "com.google.android.horologist:horologist-media-ui", version.ref = "horologist" } +horologist-roboscreenshots = { module = "com.google.android.horologist:horologist-roboscreenshots", version.ref = "horologist" } +junit = { module = "junit:junit", version.ref = "junit" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx_immutable" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } +lifecycle-runtime-ktx-v262 = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtxVersion" } +okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +okhttp3 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "play-services-wearable" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } +roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } +roborazzi-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } +rometools-modules = { module = "com.rometools:rome-modules", version.ref = "rome" } +rometools-rome = { module = "com.rometools:rome", version.ref = "rome" } +snapper = { module = "dev.chrisbanes.snapper:snapper", version.ref = "snapper" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" } +androidx-security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" } +kmp-date-time-picker = { module = "network.chaintech:kmp-date-time-picker", version.ref = "kmpDateTimePicker" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } +compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } +secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d6c8bc7 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,19 @@ +# Copyright 2023 The Android Open Source Project +# +# 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 +# +# http://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. + +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 + +@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=. +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%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/keystore/platform.keystore b/keystore/platform.keystore new file mode 100644 index 0000000..1df6237 Binary files /dev/null and b/keystore/platform.keystore differ diff --git a/screenshots/jetnews_all_screens.png b/screenshots/jetnews_all_screens.png new file mode 100644 index 0000000..74119a0 Binary files /dev/null and b/screenshots/jetnews_all_screens.png differ diff --git a/screenshots/jetnews_demo.gif b/screenshots/jetnews_demo.gif new file mode 100644 index 0000000..b3964d7 Binary files /dev/null and b/screenshots/jetnews_demo.gif differ diff --git a/screenshots/jetnews_glance_appwidget.png b/screenshots/jetnews_glance_appwidget.png new file mode 100644 index 0000000..9d2eb7f Binary files /dev/null and b/screenshots/jetnews_glance_appwidget.png differ diff --git a/screenshots/screenshots.png b/screenshots/screenshots.png new file mode 100644 index 0000000..7feed9a Binary files /dev/null and b/screenshots/screenshots.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7ad5d4d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "VasCURA589" +include(":app") + diff --git a/spotless/copyright.kt b/spotless/copyright.kt new file mode 100644 index 0000000..806db0f --- /dev/null +++ b/spotless/copyright.kt @@ -0,0 +1,16 @@ +/* + * Copyright $YEAR The Android Open Source Project + * + * 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. + */ +