From fb260289c7afeb7df65177500008d0de6d071709 Mon Sep 17 00:00:00 2001 From: SamNofee Date: Fri, 19 Jan 2024 00:32:38 +0800 Subject: [PATCH] feat(all): Init --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/.name | 1 + .idea/compiler.xml | 6 + .idea/deploymentTargetDropDown.xml | 23 + .idea/gradle.xml | 19 + .idea/kotlinc.xml | 6 + .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle | 112 ++++ app/proguard-rules.pro | 137 ++++ app/src/main/AndroidManifest.xml | 61 ++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 25715 bytes .../main/java/com/power/ops/MainActivity.kt | 75 +++ .../java/com/power/ops/MainApplication.kt | 13 + .../com/power/ops/components/BottomBar.kt | 68 ++ .../java/com/power/ops/components/Dialog.kt | 70 +++ .../java/com/power/ops/components/Divider.kt | 16 + .../java/com/power/ops/components/Dropdown.kt | 36 ++ .../com/power/ops/components/GeckoWebview.kt | 39 ++ .../java/com/power/ops/components/Header.kt | 123 ++++ .../java/com/power/ops/components/KVText.kt | 20 + .../java/com/power/ops/components/RichItem.kt | 57 ++ .../main/java/com/power/ops/components/Tip.kt | 49 ++ .../java/com/power/ops/components/Webview.kt | 45 ++ .../java/com/power/ops/managers/DjiManager.kt | 595 ++++++++++++++++++ .../com/power/ops/managers/FileManager.kt | 37 ++ .../com/power/ops/managers/GeckoManager.kt | 14 + .../com/power/ops/managers/HttpManager.kt | 144 +++++ .../java/com/power/ops/managers/LogManager.kt | 17 + .../power/ops/managers/WebsocketManager.kt | 66 ++ .../java/com/power/ops/pages/AircraftPage.kt | 255 ++++++++ .../power/ops/pages/AircraftSettingPage.kt | 113 ++++ .../main/java/com/power/ops/pages/HomePage.kt | 43 ++ .../java/com/power/ops/pages/LoginPage.kt | 137 ++++ .../java/com/power/ops/pages/MessagePage.kt | 28 + .../java/com/power/ops/pages/SettingPage.kt | 160 +++++ .../main/java/com/power/ops/pages/SitePage.kt | 28 + .../java/com/power/ops/pages/SubmitPage.kt | 26 + .../main/java/com/power/ops/theme/Color.kt | 51 ++ .../main/java/com/power/ops/theme/Theme.kt | 100 +++ .../main/java/com/power/ops/utils/Helper.kt | 51 ++ .../main/java/com/power/ops/vms/SettingVM.kt | 99 +++ .../drawable-v24/ic_launcher_foreground.xml | 63 ++ app/src/main/res/drawable/i3060.xml | 58 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1426 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3214 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 956 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1918 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 2038 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 4502 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2986 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 6958 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 4286 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 9534 bytes .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/themes.xml | 4 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + build.gradle | 26 + gradle.properties | 4 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++ gradlew.bat | 89 +++ settings.gradle | 22 + 70 files changed, 3487 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/power/ops/MainActivity.kt create mode 100644 app/src/main/java/com/power/ops/MainApplication.kt create mode 100644 app/src/main/java/com/power/ops/components/BottomBar.kt create mode 100644 app/src/main/java/com/power/ops/components/Dialog.kt create mode 100644 app/src/main/java/com/power/ops/components/Divider.kt create mode 100644 app/src/main/java/com/power/ops/components/Dropdown.kt create mode 100644 app/src/main/java/com/power/ops/components/GeckoWebview.kt create mode 100644 app/src/main/java/com/power/ops/components/Header.kt create mode 100644 app/src/main/java/com/power/ops/components/KVText.kt create mode 100644 app/src/main/java/com/power/ops/components/RichItem.kt create mode 100644 app/src/main/java/com/power/ops/components/Tip.kt create mode 100644 app/src/main/java/com/power/ops/components/Webview.kt create mode 100644 app/src/main/java/com/power/ops/managers/DjiManager.kt create mode 100644 app/src/main/java/com/power/ops/managers/FileManager.kt create mode 100644 app/src/main/java/com/power/ops/managers/GeckoManager.kt create mode 100644 app/src/main/java/com/power/ops/managers/HttpManager.kt create mode 100644 app/src/main/java/com/power/ops/managers/LogManager.kt create mode 100644 app/src/main/java/com/power/ops/managers/WebsocketManager.kt create mode 100644 app/src/main/java/com/power/ops/pages/AircraftPage.kt create mode 100644 app/src/main/java/com/power/ops/pages/AircraftSettingPage.kt create mode 100644 app/src/main/java/com/power/ops/pages/HomePage.kt create mode 100644 app/src/main/java/com/power/ops/pages/LoginPage.kt create mode 100644 app/src/main/java/com/power/ops/pages/MessagePage.kt create mode 100644 app/src/main/java/com/power/ops/pages/SettingPage.kt create mode 100644 app/src/main/java/com/power/ops/pages/SitePage.kt create mode 100644 app/src/main/java/com/power/ops/pages/SubmitPage.kt create mode 100644 app/src/main/java/com/power/ops/theme/Color.kt create mode 100644 app/src/main/java/com/power/ops/theme/Theme.kt create mode 100644 app/src/main/java/com/power/ops/utils/Helper.kt create mode 100644 app/src/main/java/com/power/ops/vms/SettingVM.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/i3060.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..4bc60f9 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Power \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..a3470d7 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..568bea1 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..c76712d --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,112 @@ +plugins { + id 'com.android.application' version '7.4.2' + id 'org.jetbrains.kotlin.android' version '1.6.20' + id 'kotlin-kapt' +} + +android { + namespace 'com.power.ops' + compileSdk 33 + + defaultConfig { + applicationId "com.power.ops" + minSdk 23 + targetSdk 33 + versionCode 17 + versionName "1.8" + + vectorDrawables { + useSupportLibrary true + } + + ndk { + abiFilters 'arm64-v8a' + } + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.2.0-alpha08' + } + + packagingOptions { + pickFirst 'lib/arm64-v8a/libc++_shared.so' + pickFirst 'lib/armeabi-v7a/libc++_shared.so' + } + + packagingOptions { + doNotStrip "*/*/libconstants.so" + doNotStrip "*/*/libdji_innertools.so" + doNotStrip "*/*/libdjibase.so" + doNotStrip "*/*/libDJICSDKCommon.so" + doNotStrip "*/*/libDJIFlySafeCore-CSDK.so" + doNotStrip "*/*/libdjifs_jni-CSDK.so" + doNotStrip "*/*/libDJIRegister.so" + doNotStrip "*/*/libdjisdk_jni.so" + doNotStrip "*/*/libDJIUpgradeCore.so" + doNotStrip "*/*/libDJIUpgradeJNI.so" + doNotStrip "*/*/libDJIWaypointV2Core-CSDK.so" + doNotStrip "*/*/libdjiwpv2-CSDK.so" + doNotStrip "*/*/libffmpeg.so" + doNotStrip "*/*/libFlightRecordEngine.so" + doNotStrip "*/*/libvideo-framing.so" + doNotStrip "*/*/libwaes.so" + doNotStrip "*/*/libagora-rtsa-sdk.so" + doNotStrip "*/*/libc++.so" + doNotStrip "*/*/libc++_shared.so" + doNotStrip "*/*/libmrtc_28181.so" + doNotStrip "*/*/libmrtc_agora.so" + doNotStrip "*/*/libmrtc_core.so" + doNotStrip "*/*/libmrtc_core_jni.so" + doNotStrip "*/*/libmrtc_data.so" + doNotStrip "*/*/libmrtc_log.so" + doNotStrip "*/*/libmrtc_onvif.so" + doNotStrip "*/*/libmrtc_rtmp.so" + doNotStrip "*/*/libmrtc_rtsp.so" + } +} + +dependencies { + implementation 'com.github.delight-im:Android-AdvancedWebView:v3.2.1' + implementation 'com.squareup.okhttp3:okhttp:4.11.0' + implementation 'com.google.code.gson:gson:2.10.1' + + implementation "org.mozilla.geckoview:geckoview-nightly:106.0.20220918094414" + + implementation 'com.dji:dji-sdk-v5-aircraft:5.6.0' + compileOnly 'com.dji:dji-sdk-v5-aircraft-provided:5.6.0' + runtimeOnly 'com.dji:dji-sdk-v5-networkImp:5.6.0' + + implementation 'androidx.navigation:navigation-compose:2.5.3' + implementation "androidx.core:core-ktx:1.9.0" + + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" + implementation "androidx.room:room-runtime:2.5.1" + implementation "androidx.room:room-ktx:2.5.1" + annotationProcessor "androidx.room:room-compiler:2.5.1" + kapt "androidx.room:room-compiler:2.5.1" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" + + implementation "androidx.activity:activity-compose:1.2.0-alpha08" + implementation "androidx.compose.material:material-icons-extended:1.2.0-alpha08" + implementation platform("androidx.compose:compose-bom:2022.10.00") + implementation "androidx.compose.ui:ui" + implementation "androidx.compose.ui:ui-tooling" + implementation "androidx.compose.ui:ui-tooling-preview" + implementation "androidx.compose.material3:material3" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..bf0e561 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,137 @@ +-keepattributes Exceptions,InnerClasses,*Annotation*,Signature,EnclosingMethod + +-dontshrink +-dontoptimize +-dontpreverify +-dontnote +-ignorewarnings + +-keepclassmembers enum * { + public static ; +} + +# 避免混淆Annotation、内部类、泛型、匿名类 +-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod + +-keepnames class * implements java.io.Serializable +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} +-keep class * extends android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-keep,allowshrinking class * extends dji.publics.DJIUI.** { + public ; +} +#加固后的AAR,其内容无法被混淆工具识别,所以MSDK外部依赖的类,必须被Keep。 +-keep class net.sqlcipher.** { *; } +-keep class net.sqlcipher.database.* { *; } +-keep class dji.** { *; } +-keep class com.dji.** { *; } +-keep class djimrtc.** { *; } +-keep class com.google.** { *; } +-keep class org.bouncycastle.** { *; } +-keep class org.** { *; } +-keep class com.squareup.wire.** { *; } +-keep class sun.misc.Unsafe { *; } +-keep class com.secneo.** {*;} +-keep class io.reactivex.**{*;} +-keep class okhttp3.**{*;} +-keep class okio.**{*;} +-keep class org.bouncycastle.**{*;} +-keep class sun.**{*;} +-keep class java.**{*;} +-keep class com.amap.api.**{*;} +-keep class com.here.**{*;} +-keep class com.mapbox.**{*;} +-keep class retrofit2.**{*;} + +-keepclasseswithmembers,allowshrinking class * { + native ; +} + +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +-keep class androidx.appcompat.widget.SearchView { *; } + +-keepclassmembers class * extends android.app.Service +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} +-keep class kotlin.** { *; } +-keep class androidx.** { *; } +-keep class android.** { *; } +-keep class com.android.** { *; } +-keep class android.media.** { *; } +-keep class okio.** { *; } +-keep class com.lmax.disruptor.** { + *; +} + +-dontwarn com.mapbox.services.android.location.LostLocationEngine +-dontwarn com.mapbox.services.android.location.MockLocationEngine +-keepclassmembers class * implements android.arch.lifecycle.LifecycleObserver { + (...); +} +# ViewModel's empty constructor is considered to be unused by proguard +-keepclassmembers class * extends android.arch.lifecycle.ViewModel { + (...); +} +# keep Lifecycle State and Event enums values +-keepclassmembers class android.arch.lifecycle.Lifecycle$State { *; } +-keepclassmembers class android.arch.lifecycle.Lifecycle$Event { *; } +# keep methods annotated with @OnLifecycleEvent even if they seem to be unused +# (Mostly for LiveData.LifecycleBoundObserver.onStateChange(), but who knows) +-keepclassmembers class * { + @android.arch.lifecycle.OnLifecycleEvent *; +} + +-keepclassmembers class * implements android.arch.lifecycle.LifecycleObserver { + (...); +} + +-keep class * implements android.arch.lifecycle.LifecycleObserver { + (...); +} +-keepclassmembers class android.arch.** { *; } +-keep class android.arch.** { *; } +-dontwarn android.arch.** + +-dontwarn dalvik.** +-dontwarn com.tencent.smtt.** +-keep class com.tencent.smtt.** { + *; +} +-keep class com.tencent.tbs.** { + *; +} +#-keep class * extends android.webkit.WebChromeClient { *; } +#-dontwarn im.delight.android.webview.** + +#<------------ utmiss config start------------> +-keep class dji.sdk.utmiss.** { *; } +-keep class utmisslib.** { *; } +#<------------ utmiss config end------------> + +# 地图相关 +-keep class com.dji.mapkit.amap.provider.AMapProvider {*;} +-keep class com.dji.mapkit.maplibre.provider.MapLibreProvider {*;} +-keep class com.dji.mapkit.core.** {*;} +-keep class com.autonavi.** {*;} + + +## keep 千寻相关接口 +-keep class com.qx.wz.dj.rtcm.** {*;} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..34e6871 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..1cfee899441b531cf624ed3ddfcd62c91a5e2144 GIT binary patch literal 25715 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 + BottomBarItem(Icons.Rounded.Message, "消息", "MessagePage"), + BottomBarItem(Icons.Rounded.FormatListBulleted, "工单","SubmitPage"), + BottomBarItem(Icons.Rounded.Home, "首页", "HomePage"), + BottomBarItem(Icons.Rounded.CrisisAlert, "全景", "SitePage"), + BottomBarItem(Icons.Rounded.Settings, "设置", "SettingPage") + ) + +// NavigationBar( +// containerColor = Main500 +// ) { +// items.forEach { +// NavigationBarItem( +// selected = selected == it.value, +// onClick = { +// redirectTo(navController, it.value) +// }, +// icon = { +// Icon(it.icon, contentDescription = null) +// }, +// alwaysShowLabel = selected == it.value, +// label = { Text(it.label, fontWeight = FontWeight.Bold, style = Typography.labelSmall) }, +// colors = NavigationBarItemDefaults.colors(indicatorColor = Secondary400) +// ) +// } +// } +} + +@Preview +@Composable +fun PreviewBottomBar() { + PowerTheme { + BottomBar(NavController(LocalContext.current), "HomePage") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/components/Dialog.kt b/app/src/main/java/com/power/ops/components/Dialog.kt new file mode 100644 index 0000000..2f41f7e --- /dev/null +++ b/app/src/main/java/com/power/ops/components/Dialog.kt @@ -0,0 +1,70 @@ +package com.power.ops.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.power.ops.theme.Char600 +import com.power.ops.theme.Primary500 +import com.power.ops.theme.Secondary400 +import com.power.ops.theme.Typography + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Dialog( + showDialog: Boolean, + title: String = "请输入", + defaultValue: String = "", + onDismiss: () -> Unit, + onConfirm: (String) -> Unit +) { + var inputValue by remember { mutableStateOf(defaultValue) } + + if (showDialog) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = title, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = Typography.titleLarge) }, + text = { + Column { + OutlinedTextField( + value = inputValue, + onValueChange = { inputValue = it }, + modifier = Modifier + .padding(top = 14.dp), + colors = TextFieldDefaults.outlinedTextFieldColors(unfocusedBorderColor = Char600, focusedBorderColor = Primary500), + keyboardActions = KeyboardActions(onDone = { onConfirm(inputValue) }), + ) + } + }, + confirmButton = { + TextButton(onClick = { + onConfirm(inputValue) + onDismiss() + }) { + Text(text = "确认") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = "取消") + } + }, + containerColor = Secondary400 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/components/Divider.kt b/app/src/main/java/com/power/ops/components/Divider.kt new file mode 100644 index 0000000..fa83370 --- /dev/null +++ b/app/src/main/java/com/power/ops/components/Divider.kt @@ -0,0 +1,16 @@ +package com.power.ops.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +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.Color +import androidx.compose.ui.unit.dp +import com.power.ops.theme.Char500 + +@Composable +fun Divider(color: Color = Char500.copy(0.05f)) { + Row( modifier = Modifier.fillMaxWidth().height(0.5.dp).background(color) ) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/components/Dropdown.kt b/app/src/main/java/com/power/ops/components/Dropdown.kt new file mode 100644 index 0000000..eeadff5 --- /dev/null +++ b/app/src/main/java/com/power/ops/components/Dropdown.kt @@ -0,0 +1,36 @@ +package com.power.ops.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import com.power.ops.theme.Secondary400 + +data class DropdownItem(val name: String, val action: () -> Unit) + +@Composable +fun Dropdown(imageVector: ImageVector, items: Array) { + var isShowMenu by remember { mutableStateOf(false) } + Box { + IconButton(onClick = { isShowMenu = true }) { Icon(imageVector, contentDescription = null) } + DropdownMenu( + expanded = isShowMenu, + onDismissRequest = { isShowMenu = false }, + modifier = Modifier.background(Secondary400) + ) { + items.forEach { + DropdownMenuItem(text = { Text(it.name) }, onClick = it.action) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/components/GeckoWebview.kt b/app/src/main/java/com/power/ops/components/GeckoWebview.kt new file mode 100644 index 0000000..51073ae --- /dev/null +++ b/app/src/main/java/com/power/ops/components/GeckoWebview.kt @@ -0,0 +1,39 @@ +package com.power.ops.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.power.ops.managers.GeckoManager +import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.* +import org.mozilla.geckoview.GeckoSession.PromptDelegate.ButtonPrompt.Type.NEGATIVE +import org.mozilla.geckoview.GeckoSession.PromptDelegate.ButtonPrompt.Type.POSITIVE +import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt.Type.MULTIPLE +import org.mozilla.geckoview.GeckoView +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.WebResponse + +@Composable +fun GeckoWebview(modifier: Modifier = Modifier) { + AndroidView( + modifier = Modifier.fillMaxWidth(), // Occupy the max size in the Compose UI tree + factory = { context -> + // Creates custom view + GeckoView(context).apply { + GeckoManager.singleton.runtime = GeckoRuntime.create(context) + GeckoManager.singleton.session.open(GeckoManager.singleton.runtime) + this.releaseSession() + this.setSession(GeckoManager.singleton.session) + + GeckoManager.singleton.session.loadUri("http://nofee.fun") + } + }, + update = { view -> + // View's been inflated or state read in this block has been updated + // Add logic here if necessary + view.releaseSession() + view.setSession(GeckoManager.singleton.session) + } + ) +} diff --git a/app/src/main/java/com/power/ops/components/Header.kt b/app/src/main/java/com/power/ops/components/Header.kt new file mode 100644 index 0000000..23f5f34 --- /dev/null +++ b/app/src/main/java/com/power/ops/components/Header.kt @@ -0,0 +1,123 @@ +package com.power.ops.components + +import androidx.compose.foundation.Image +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.material.icons.rounded.Dashboard +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import com.power.ops.R +import com.power.ops.theme.Char400 +import com.power.ops.theme.Main500 +import com.power.ops.theme.Secondary400 +import com.power.ops.theme.Typography +import com.power.ops.utils.redirectTo + +enum class HeaderType { + CONFIRM, MAIN, NORMAL +} + +@Composable +fun Header( + navController: NavController, + headerType: HeaderType, + title: String, + height: Dp = 54.dp, + fontSize: TextUnit = 20.sp, + action: () -> Unit = {} +) { + var isShowMenu by remember { mutableStateOf(false) } + + Row( + modifier = Modifier + .fillMaxWidth() + .height(height) + .background(Main500) + .zIndex(1f), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(15.dp)) + if (headerType == HeaderType.CONFIRM) { + IconButton(onClick = { + navController.popBackStack() + }, modifier = Modifier.width(35.dp)) { + Icon(Icons.Filled.KeyboardArrowLeft, contentDescription = null) + } + } else if (headerType == HeaderType.MAIN) { + Image( + painter = painterResource(id = R.drawable.i3060), + contentDescription = null, + modifier = Modifier + .size(25.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } + + Spacer(modifier = Modifier.width(15.dp)) + Text(title, style = TextStyle( + color = Char400, + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = fontSize + )) + Spacer(modifier = Modifier.weight(1f)) + + if (headerType == HeaderType.CONFIRM) { + IconButton(onClick = action, modifier = Modifier.width(35.dp)) { + Icon(Icons.Rounded.CheckCircle, contentDescription = null) + } + } else if (headerType == HeaderType.MAIN) { + Box { + IconButton(onClick = { isShowMenu = true }) { Icon(Icons.Rounded.Dashboard, contentDescription = null) } + DropdownMenu( + expanded = isShowMenu, + onDismissRequest = { isShowMenu = false }, + modifier = Modifier.background(Secondary400) + ) { + listOf("设置").forEach { item -> + DropdownMenuItem(text = { Text(item) }, onClick = { + isShowMenu = false + if (item == "设置") { + navController.navigate("SettingPage") + } + }) + } + } + } + } + Spacer(modifier = Modifier.width(15.dp)) + } +} diff --git a/app/src/main/java/com/power/ops/components/KVText.kt b/app/src/main/java/com/power/ops/components/KVText.kt new file mode 100644 index 0000000..2f63534 --- /dev/null +++ b/app/src/main/java/com/power/ops/components/KVText.kt @@ -0,0 +1,20 @@ +package com.power.ops.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.power.ops.theme.Char600 +import com.power.ops.theme.Char700 + + +@Composable +fun KVText(key: String, value: String) { + Row(modifier = Modifier.padding(vertical = 3.dp)) { + Text(key, color = Char700, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 3.dp)) + Text(value, color = Char600, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 3.dp), lineHeight = 12.sp) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/components/RichItem.kt b/app/src/main/java/com/power/ops/components/RichItem.kt new file mode 100644 index 0000000..f192a7e --- /dev/null +++ b/app/src/main/java/com/power/ops/components/RichItem.kt @@ -0,0 +1,57 @@ +package com.power.ops.components + +import androidx.compose.foundation.clickable +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.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.rounded.Route +import androidx.compose.material3.Icon +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.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.power.ops.theme.Char700 +import com.power.ops.theme.PowerTheme + +@Composable +fun RichItem( + imageVector: ImageVector, + text: String, + intro: String? = null, + action: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .clickable(onClick = action), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(10.dp)) + Icon(imageVector, contentDescription = null) + Spacer(modifier = Modifier.width(10.dp)) + Text(text) + Spacer(modifier = Modifier.weight(1f)) + if (intro != null) { + Text(intro, color = Char700) + Spacer(modifier = Modifier.width(10.dp)) + } + Icon(Icons.Filled.KeyboardArrowRight, contentDescription = null) + Spacer(modifier = Modifier.width(10.dp)) + } +} + +@Preview +@Composable +fun PreviewRichItem() { + PowerTheme { + RichItem(Icons.Rounded.Route, "头部", "20.3", action = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/components/Tip.kt b/app/src/main/java/com/power/ops/components/Tip.kt new file mode 100644 index 0000000..eb8afcb --- /dev/null +++ b/app/src/main/java/com/power/ops/components/Tip.kt @@ -0,0 +1,49 @@ +package com.power.ops.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.power.ops.theme.Char500 +import com.power.ops.theme.Main500 +import com.power.ops.theme.PowerTheme + +@Composable +fun Tip( + showTip: Boolean, + text: String, + action: () -> Unit +) { + if (showTip) { + Snackbar( + modifier = Modifier.padding(14.dp), + containerColor = Main500, + dismissAction = { + IconButton( + onClick = action, + colors = IconButtonDefaults.iconButtonColors(contentColor = Char500) + ) { + Icon(Icons.Rounded.Close, contentDescription = null) + } + } + ) { + Text(text) + } + } +} + +@Preview +@Composable +fun PreviewTip() { + PowerTheme { + Tip(true, "测试", {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/components/Webview.kt b/app/src/main/java/com/power/ops/components/Webview.kt new file mode 100644 index 0000000..bcd0ebb --- /dev/null +++ b/app/src/main/java/com/power/ops/components/Webview.kt @@ -0,0 +1,45 @@ +package com.power.ops.components + +import android.view.ViewGroup +import android.webkit.WebSettings +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.power.ops.managers.HttpManager +import com.power.ops.theme.Char400 +import com.power.ops.utils.ip +import im.delight.android.webview.AdvancedWebView + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Webview(uri: String) { + val coroutineScope = rememberCoroutineScope() + + AndroidView( + factory = { context -> + AdvancedWebView(context).apply { + this.clearCache(true) + + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + + settings.javaScriptEnabled = true + settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + settings.loadsImagesAutomatically = true + settings.allowContentAccess = true + settings.domStorageEnabled = true + settings.allowFileAccess = true + setCookiesEnabled(true) + setMixedContentAllowed(true) + settings.useWideViewPort = true + settings.loadWithOverviewMode = true + } + }, + modifier = Modifier.fillMaxHeight().background(Char400) + ) { webview -> + webview.loadUrl("https://$ip$uri?token=${HttpManager.singleton.token}") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/managers/DjiManager.kt b/app/src/main/java/com/power/ops/managers/DjiManager.kt new file mode 100644 index 0000000..cca9284 --- /dev/null +++ b/app/src/main/java/com/power/ops/managers/DjiManager.kt @@ -0,0 +1,595 @@ +package com.power.ops.managers + +import android.content.Context +import android.view.SurfaceHolder +import android.view.SurfaceView +import androidx.compose.animation.core.updateTransition +import com.dji.wpmzsdk.common.data.Template +import com.dji.wpmzsdk.manager.WPMZManager +import com.google.gson.Gson +import com.power.ops.utils.bitmapToBase64 +import com.power.ops.utils.calculateMD5 +import com.power.ops.vms.SettingVM +import dji.sdk.keyvalue.key.AirLinkKey +import dji.sdk.keyvalue.key.BatteryKey +import dji.sdk.keyvalue.key.CameraKey +import dji.sdk.keyvalue.key.FlightControllerKey +import dji.sdk.keyvalue.key.GimbalKey +import dji.sdk.keyvalue.key.KeyTools +import dji.sdk.keyvalue.value.camera.CameraMode +import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType +import dji.sdk.keyvalue.value.camera.PhotoFileFormat +import dji.sdk.keyvalue.value.camera.PhotoSize +import dji.sdk.keyvalue.value.common.LocationCoordinate2D +import dji.sdk.keyvalue.value.common.LocationCoordinate3D +import dji.sdk.keyvalue.value.gimbal.GimbalSpeedRotation + +// import dji.sdk.keyvalue.value.mission.WaypointMission + +import dji.sdk.wpmz.value.mission.ActionGimbalRotateParam; +import dji.sdk.wpmz.value.mission.ActionStartRecordParam; +import dji.sdk.wpmz.value.mission.ActionStopRecordParam; +import dji.sdk.wpmz.value.mission.ActionTakePhotoParam; +import dji.sdk.wpmz.value.mission.CameraLensType; +import dji.sdk.wpmz.value.mission.Wayline; +import dji.sdk.wpmz.value.mission.WaylineActionGroup; +import dji.sdk.wpmz.value.mission.WaylineActionInfo; +import dji.sdk.wpmz.value.mission.WaylineActionNodeList; +import dji.sdk.wpmz.value.mission.WaylineActionTreeNode; +import dji.sdk.wpmz.value.mission.WaylineActionTrigger; +import dji.sdk.wpmz.value.mission.WaylineActionTriggerType; +import dji.sdk.wpmz.value.mission.WaylineActionType; +import dji.sdk.wpmz.value.mission.WaylineActionsRelationType; +import dji.sdk.wpmz.value.mission.WaylineAltitudeMode; +import dji.sdk.wpmz.value.mission.WaylineCoordinateMode; +import dji.sdk.wpmz.value.mission.WaylineCoordinateParam; +import dji.sdk.wpmz.value.mission.WaylineDroneInfo; +import dji.sdk.wpmz.value.mission.WaylineExecuteAltitudeMode; +import dji.sdk.wpmz.value.mission.WaylineExecuteWaypoint; +import dji.sdk.wpmz.value.mission.WaylineExitOnRCLostAction; +import dji.sdk.wpmz.value.mission.WaylineExitOnRCLostBehavior; +import dji.sdk.wpmz.value.mission.WaylineFinishedAction; +import dji.sdk.wpmz.value.mission.WaylineFlyToWaylineMode; +import dji.sdk.wpmz.value.mission.WaylineGimbalActuatorRotateMode; +import dji.sdk.wpmz.value.mission.WaylineLocationCoordinate2D; +import dji.sdk.wpmz.value.mission.WaylineMission; +import dji.sdk.wpmz.value.mission.WaylineMissionConfig; +import dji.sdk.wpmz.value.mission.WaylinePayloadInfo; +import dji.sdk.wpmz.value.mission.WaylinePositioningType; +import dji.sdk.wpmz.value.mission.WaylineTemplateWaypointInfo; +import dji.sdk.wpmz.value.mission.WaylineWaypoint; +import dji.sdk.wpmz.value.mission.WaylineWaypointPitchMode; +import dji.sdk.wpmz.value.mission.WaylineWaypointTurnParam; +import dji.sdk.wpmz.value.mission.WaylineWaypointYawMode; +import dji.sdk.wpmz.value.mission.WaylineWaypointYawParam; + + +import dji.v5.common.callback.CommonCallbacks +import dji.v5.common.callback.CommonCallbacks.CompletionCallback +import dji.v5.common.error.IDJIError +import dji.v5.common.register.DJISDKInitEvent +import dji.v5.common.video.channel.VideoChannelState +import dji.v5.common.video.channel.VideoChannelType +import dji.v5.common.video.decoder.DecoderOutputMode +import dji.v5.common.video.decoder.VideoDecoder +import dji.v5.et.action +import dji.v5.et.get +import dji.v5.et.listen +import dji.v5.et.set +import dji.v5.manager.SDKManager +import dji.v5.manager.aircraft.virtualstick.VirtualStickManager +import dji.v5.manager.aircraft.waypoint3.WaypointMissionManager +import dji.v5.manager.datacenter.MediaDataCenter +import dji.v5.manager.datacenter.livestream.LiveStreamSettings +import dji.v5.manager.datacenter.livestream.LiveStreamType +import dji.v5.manager.datacenter.livestream.settings.AgoraSettings +import dji.v5.manager.datacenter.livestream.settings.RtmpSettings +import dji.v5.manager.datacenter.media.MediaFile +import dji.v5.manager.datacenter.media.MediaFileDownloadListener +import dji.v5.manager.datacenter.media.MediaFileListState +import dji.v5.manager.datacenter.media.PullMediaFileListParam +import dji.v5.manager.datacenter.video.VideoStreamManager +import dji.v5.manager.interfaces.SDKManagerCallback +import dji.sdk.wpmz.value.mission.* +import dji.v5.utils.common.FileUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.BufferedOutputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +data class KMZPoint(val lon: Double, val lat: Double) +data class KMZPoints(val points: ArrayList) +data class FileListItem(val name: String, val createdTime: String) + +class DjiManager { + val videoStreamManager = VideoStreamManager.getInstance() + val mediaManager = MediaDataCenter.getInstance().mediaManager + val liveStreamManager = MediaDataCenter.getInstance().liveStreamManager + val wpmzManager = WPMZManager.getInstance() + + var airlineUrl = "" + var airlineFile: File? = null + + var fileList: MutableList = mutableListOf() + + var videoCtx: Context? = null + var videoView: SurfaceView? = null + var videoHolder: SurfaceHolder? = null + + fun getChannelStatus(): String { + val videoChannel = videoStreamManager.getAvailableVideoChannel(VideoChannelType.PRIMARY_STREAM_CHANNEL) + if (videoChannel != null ) { + return videoChannel.videoChannelStatus.toString() + } else { + return "无法初始化" + } + } + + fun loadVideoSurface(ctx: Context, holder: SurfaceHolder, view: SurfaceView) { + videoCtx = ctx + videoHolder = holder + videoView = view + val videoChannel = videoStreamManager.getAvailableVideoChannel(VideoChannelType.PRIMARY_STREAM_CHANNEL) + if (videoChannel != null && videoChannel.videoChannelStatus == VideoChannelState.ON) { + VideoDecoder( + ctx, VideoChannelType.PRIMARY_STREAM_CHANNEL, DecoderOutputMode.SURFACE_MODE, + holder, -1, -1, true + ) + } + } + + fun setRtmp(url: String) { + val rtmpSettings = RtmpSettings.Builder() + .setUrl(url) + .build() + val liveStreamSettings = LiveStreamSettings.Builder() + .setLiveStreamType(LiveStreamType.RTMP) + .setRtmpSettings(rtmpSettings) + .build() + liveStreamManager.liveStreamSettings = liveStreamSettings + } + + fun setAgora(channelId: String, token: String, uid: String) { + val agoraConfig = AgoraSettings.Builder() + .setChannelId(channelId) + .setToken(token) + .setUid(uid) + .setEnableSafety(false) + .build() + val liveStreamSettings = LiveStreamSettings.Builder() + .setLiveStreamType(LiveStreamType.AGORA) + .setAgoraSettings(agoraConfig) + .build() + liveStreamManager.liveStreamSettings = liveStreamSettings + } + + fun startLiveStream(onSuccess: () -> Unit, onFailure: (error: String) -> Unit) { + liveStreamManager.startStream(object : CommonCallbacks.CompletionCallback { + override fun onSuccess() { onSuccess() } + override fun onFailure(error: IDJIError) { onFailure(error.toString()) } + }) + } + + fun enableStick() { + VirtualStickManager.getInstance().enableVirtualStick(object: CommonCallbacks.CompletionCallback { + override fun onSuccess() { LogManager.i("Enable success") } + override fun onFailure(error: IDJIError) { LogManager.i("Enable stick fail: $error") } + }) + } + + fun disableStick() { + VirtualStickManager.getInstance().disableVirtualStick(object: CommonCallbacks.CompletionCallback { + override fun onSuccess() { LogManager.i("Disable success") } + override fun onFailure(error: IDJIError) { LogManager.i("Disable stick fail: $error") } + }) + } + + fun changeRightStick(x: Int?, y: Int?) { + val stick = VirtualStickManager.getInstance().rightStick + x?.let { stick.horizontalPosition = x } + y?.let { stick.verticalPosition = y } + } + + fun changeLeftStick(x: Int?, y: Int?) { + val stick = VirtualStickManager.getInstance().leftStick + x?.let { stick.horizontalPosition = x } + y?.let { stick.verticalPosition = y } + } + + fun uploadKMZ() { + airlineFile?.let { + WaypointMissionManager.getInstance().pushKMZFileToAircraft(it.path, object : CommonCallbacks.CompletionCallbackWithProgress { + override fun onProgressUpdate(progress: Double?) { LogManager.i("Upload KMZ progress: $progress") } + override fun onSuccess() { LogManager.i("Upload KMZ success") } + override fun onFailure(error: IDJIError) { LogManager.i("Upload KMZ error: $error") } + }) + } + } + + fun startMission() { + airlineFile?.let { + val name = FileUtils.getFileName(it.path, ".kmz") + val ids = WaypointMissionManager.getInstance().getAvailableWaylineIDs(it.path) + WaypointMissionManager.getInstance().startMission(name, ids, object : CommonCallbacks.CompletionCallback { + override fun onSuccess() { LogManager.i("Success start mission") } + override fun onFailure(error: IDJIError) { LogManager.i("Fail start mission: $error") } + }) + } + } + + fun pauseMission() { + WaypointMissionManager.getInstance().pauseMission(object : CommonCallbacks.CompletionCallback { + override fun onSuccess() { LogManager.i("Success pause mission") } + override fun onFailure(error: IDJIError) { LogManager.i("Pause mission: $error") } + }) + } + + fun resumeMission() { + WaypointMissionManager.getInstance().resumeMission(object : CommonCallbacks.CompletionCallback { + override fun onSuccess() { LogManager.i("Success resume mission") } + override fun onFailure(error: IDJIError) { LogManager.i("Resume mission: $error") } + }) + } + + fun stopMission() { + airlineFile?.let { + val name = FileUtils.getFileName(it.path, ".kmz") + WaypointMissionManager.getInstance().stopMission(name, object : CommonCallbacks.CompletionCallback { + override fun onSuccess() { LogManager.i("Success stop mission") } + override fun onFailure(error: IDJIError) { LogManager.i("Stop mission: $error") } + }) + } + } + + fun addMissionStateListener() { + WaypointMissionManager.getInstance().addWaypointMissionExecuteStateListener { + WebsocketManager.singleton.sendPayload("KeyWaypointMissionExecuteState", it.name ) + } + } + + fun changeGimbalRotate(value: Double) { + val rotation = GimbalSpeedRotation() + rotation.pitch = value + rotation.yaw = 0.0 + rotation.roll = 0.0 + KeyTools.createKey(GimbalKey.KeyRotateBySpeed).action(rotation, onSuccess = { + LogManager.i(it.toString()) + }) + } + + fun changeCameraType(type: CameraVideoStreamSourceType) { + KeyTools.createKey(CameraKey.KeyCameraVideoStreamSource).set(type) + } + + fun changeCameraZoom(ratio: Double) { + KeyTools.createKey(CameraKey.KeyCameraZoomRatios).set(ratio) + } + + fun takePhoto() { + KeyTools.createKey(CameraKey.KeyCameraMode).set(CameraMode.PHOTO_NORMAL) + KeyTools.createKey(CameraKey.KeyStartShootPhoto).action() + } + + fun initPhotoFormat() { + KeyTools.createKey(CameraKey.KeyPhotoFileFormat).set(PhotoFileFormat.JPEG) + KeyTools.createKey(CameraKey.KeyPhotoSize).set(PhotoSize.SIZE_DEFAULT) + } + + fun updateFileList() { + val mediaFileListState = mediaManager.mediaFileListState + if (mediaFileListState == MediaFileListState.UP_TO_DATE) { + fileList = mediaManager.mediaFileListData.data + LogManager.i("Get FileList size ${fileList.size}") + val newList: List = fileList.map { + FileListItem(it.fileName, it.date.toString()) + } + WebsocketManager.singleton.sendPayload("FileList", Gson().toJson(newList)) + } else if (mediaFileListState == MediaFileListState.IDLE) { + mediaManager.pullMediaFileListFromCamera(PullMediaFileListParam.Builder().build(), object : CompletionCallback { + override fun onSuccess() { + fileList = mediaManager.mediaFileListData.data + LogManager.i("Pull FileList size ${fileList.size}") + val newList: List = fileList.map { + FileListItem(it.fileName, it.date.toString()) + } + WebsocketManager.singleton.sendPayload("FileList", Gson().toJson(newList)) + } + override fun onFailure(error: IDJIError) { LogManager.i("Pull media error: $error") } + }) + } + } + + fun enableMedia() { + mediaManager.enable(object : CompletionCallback { + override fun onSuccess() { updateFileList() } + override fun onFailure(error: IDJIError) { LogManager.i("Enable media fail: $error") } + }) + } + + fun disableMedia() { + mediaManager.disable(object : CompletionCallback { + override fun onSuccess() { LogManager.i("Disable media success") } + override fun onFailure(error: IDJIError) { LogManager.i("Disable media fail: $error") } + }) + } + + fun pullOriginMedia(fileName: String) { + val item = fileList.find { it.fileName == fileName } + if (item != null) { + val file = File(FileManager.singleton.tempDir, item.fileName) + var offset = 0L + if (file.exists()) { offset = file.length() } + val outputStream = FileOutputStream(file,true) + val bos = BufferedOutputStream(outputStream) + item.pullOriginalMediaFileFromCamera(offset, object : + MediaFileDownloadListener { + override fun onStart() {} + override fun onProgress(total: Long, current: Long) {} + override fun onRealtimeDataUpdate(data: ByteArray, position: Long) { + try { + bos.write(data) + bos.flush() + } catch (error: IOException) { + LogManager.i("Write bos error $error") + } + } + + override fun onFinish() { + LogManager.i("Successful downloaded") + try { + outputStream.close() + bos.close() + } catch (error: IOException) { + LogManager.i("Close error $error") + } finally { + CoroutineScope(Dispatchers.IO).launch { + val res = HttpManager.singleton.uploadJpg(file.readBytes()) + res?.let { HttpManager.singleton.reqCreateAirtaskPhoto(item.fileName, it.file) } + } + } + } + + override fun onFailure(error: IDJIError?) { LogManager.i("Pull origin error $error") } + }) + } + } + + fun clearCamera() { + updateFileList() + deleteCameraFiles(fileList) + } + + fun deleteCameraFiles(list: MutableList) { + mediaManager.deleteMediaFiles(list, object : CompletionCallback { + override fun onSuccess() { LogManager.i("Delete camera files success") } + override fun onFailure(error: IDJIError) { LogManager.i("Delete camera files error: $error") } + }) + } + + fun changeHomeLocation(lon: Double, lat: Double) { + val location = LocationCoordinate2D() + location.latitude = lat + location.longitude = lon + KeyTools.createKey(FlightControllerKey.KeyHomeLocation).set(location) + } + + fun changeFlyStatus(status: String) { + if (status == "START_FLY") { KeyTools.createKey(FlightControllerKey.KeyStartTakeoff).action() } + if (status == "GO_HOME") { KeyTools.createKey(FlightControllerKey.KeyStartGoHome).action() } + if (status == "TAKE_OFF") { KeyTools.createKey(FlightControllerKey.KeyStartAutoLanding).action() } + if (status == "TAKE_OFF_CONFIRM") { KeyTools.createKey(FlightControllerKey.KeyConfirmLanding).action() } + } + + fun getLocation(): LocationCoordinate3D? { + return KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D).get() + } + + fun generateKMZ() { + val points = listOf( + KMZPoint(113.39986078791, 23.113859777049), + KMZPoint(113.40060124500, 23.113874582813) + ) + + val waylineMission = WaylineMission() + waylineMission.setCreateTime(System.currentTimeMillis().toDouble()) + waylineMission.setUpdateTime(System.currentTimeMillis().toDouble()) + + val waylineMissionConfig = WaylineMissionConfig() + waylineMissionConfig.flyToWaylineMode = WaylineFlyToWaylineMode.SAFELY + waylineMissionConfig.finishAction = WaylineFinishedAction.GO_HOME + waylineMissionConfig.droneInfo = WaylineDroneInfo() + waylineMissionConfig.securityTakeOffHeight = 20.0 + waylineMissionConfig.isSecurityTakeOffHeightSet = true + waylineMissionConfig.exitOnRCLostBehavior = WaylineExitOnRCLostBehavior.EXCUTE_RC_LOST_ACTION + waylineMissionConfig.exitOnRCLostType = WaylineExitOnRCLostAction.GO_BACK + waylineMissionConfig.globalTransitionalSpeed = 10.0 + // waylineMissionConfig.payloadInfo = arrayListOf() + + val wayline = Wayline() + wayline.autoFlightSpeed = 5.0 + wayline.waypoints = points.map(transform = { + val point = WaylineExecuteWaypoint() + point.location = WaylineLocationCoordinate2D(it.lat, it.lon) + point + }) + + } + + // 被动执行收到的 Websocket Payload 命令 + fun execute(payload: Payload) { + try { + payload.data["GetLogs"]?.let { + CoroutineScope(Dispatchers.IO).launch { + HttpManager.singleton.reqSendLogs(LogManager.logs) + } + } + + payload.data["TakePhoto"]?.let { takePhoto() } + payload.data["UpdateFileList"]?.let { updateFileList() } + payload.data["PullOriginMedia"]?.let { pullOriginMedia(it) } + payload.data["StartLiveStream"]?.let { + CoroutineScope(Dispatchers.IO).launch { + val channelId = SettingVM.singleton.getSetting("agoraChannelId") + val token = SettingVM.singleton.getSetting("agoraToken") + val uid = SettingVM.singleton.getSetting("agoraUid") + if (channelId != null && token != null && uid != null) { + setAgora(channelId.value, token.value, uid.value) + startLiveStream(onSuccess = {}, onFailure = { error -> LogManager.i(error) }) + } + } + } + payload.data["ExecuteAirline"]?.let { + if (it == "START") { startMission() } + if (it == "STOP") { stopMission() } + if (it == "RESUME") { resumeMission() } + if (it == "PAUSE") { pauseMission() } + } + + payload.data["GenerateKMZ"]?.let { + val res = Gson().fromJson(it, KMZPoints::class.java) + + } + + payload.data["ChangeStick"]?.let { + if (it == "ENABLE") { enableStick() } + if (it == "DISABLE") { disableStick() } + } + payload.data["ChangeCameraZoom"]?.let { changeCameraZoom(it.toDouble()) } + payload.data["ChangeFlyStatus"]?.let { changeFlyStatus(it) } + payload.data["ChangeHomeLocationLatitude"]?.let { lat -> + payload.data["ChangeHomeLocationLongitude"]?.let { lon -> + changeHomeLocation(lon.toDouble(), lat.toDouble()) + } + } + payload.data["ChangeAgoraChannelId"]?.let { + CoroutineScope(Dispatchers.IO).launch { SettingVM.singleton.setSetting("agoraChannelId", it) } + } + payload.data["ChangeAgoraToken"]?.let { + CoroutineScope(Dispatchers.IO).launch { SettingVM.singleton.setSetting("agoraToken", it) } + } + payload.data["ChangeAgoraUid"]?.let { + CoroutineScope(Dispatchers.IO).launch { SettingVM.singleton.setSetting("agoraUid", it) } + } + payload.data["ChangeRightStickX"]?.let { changeRightStick(it.toInt(), null) } + payload.data["ChangeRightStickY"]?.let { changeRightStick(null, it.toInt()) } + payload.data["ChangeLeftStickX"]?.let { changeLeftStick(it.toInt(), null) } + payload.data["ChangeLeftStickY"]?.let { changeLeftStick(null, it.toInt()) } + payload.data["ChangeGimbalPitch"]?.let { changeGimbalRotate(it.toDouble()) } + payload.data["ChangeAirlineUrl"]?.let { + CoroutineScope(Dispatchers.IO).launch { + airlineUrl = it + val file = File(FileManager.singleton.airlineDir, "${calculateMD5(it)}.kmz") + HttpManager.singleton.downloadFile(file, it) + if (file.exists()) { + DjiManager.singleton.airlineFile = file + uploadKMZ() + } + } + } + payload.data["ChangeCameraType"]?.let { + if (it == "ZOOM") { changeCameraType(CameraVideoStreamSourceType.ZOOM_CAMERA) } + if (it == "WIDE") { changeCameraType(CameraVideoStreamSourceType.WIDE_CAMERA) } + if (it == "INFRARED") { changeCameraType(CameraVideoStreamSourceType.INFRARED_CAMERA) } + } + } catch (error: Error) { + LogManager.i("Execute payloads: $error") + } + } + + // 主动发送 Websocket Payload 命令 + fun reportKeys( + onChangeAltitude: (Double) -> Unit, + onChangeTakeOffAltitude: (Double) -> Unit, + onChangeCharger: (Int) -> Unit, + onChangeAirlinkSignal: (Int) -> Unit + ) { + // 飞行器 + KeyTools.createKey(FlightControllerKey.KeyIsFlying).listen(this, false) { + WebsocketManager.singleton.sendPayload("KeyIsFlying", it.toString()) + } + KeyTools.createKey(FlightControllerKey.KeyFlightTimeInSeconds).listen(this, false) { + WebsocketManager.singleton.sendPayload("KeyFlightTimeInSeconds", it.toString()) + } + KeyTools.createKey(FlightControllerKey.KeyWindSpeed).listen(this, false) { + WebsocketManager.singleton.sendPayload("KeyWindSpeed", it.toString()) + } + KeyTools.createKey(FlightControllerKey.KeyGPSSignalLevel).listen(this, false) { + WebsocketManager.singleton.sendPayload("KeyGPSSignalLevel", it.toString()) + } + KeyTools.createKey(FlightControllerKey.KeyTakeoffLocationAltitude).listen(this, false) { + it?.let { onChangeTakeOffAltitude(it) } + WebsocketManager.singleton.sendPayload("KeyTakeoffLocationAltitude", it.toString()) + } + KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D).listen(this, false) { + it?.let { + onChangeAltitude(it.altitude) + WebsocketManager.singleton.sendPayloads( mapOf( + "KeyAircraftLocation3DAltitude" to it.altitude.toString(), + "KeyAircraftLocation3DLatitude" to it.latitude.toString(), + "KeyAircraftLocation3DLongitude" to it.longitude.toString(), + )) + } + } + KeyTools.createKey(FlightControllerKey.KeyAircraftAttitude).listen(this, false) { + it?.let { + WebsocketManager.singleton.sendPayloads( mapOf( + "KeyAircraftAttitudePitch" to it.pitch.toString(), + "KeyAircraftAttitudeRoll" to it.roll.toString(), + "KeyAircraftAttitudeYaw" to it.yaw.toString(), + )) + } + } + KeyTools.createKey(FlightControllerKey.KeyAircraftVelocity).listen(this, false) { + it?.let { + WebsocketManager.singleton.sendPayloads( mapOf( + "KeyAircraftVelocityX" to it.x.toString(), + "KeyAircraftVelocityY" to it.y.toString(), + "KeyAircraftVelocityZ" to it.z.toString(), + )) + } + } + + // 云台 + KeyTools.createKey(GimbalKey.KeyYawRelativeToAircraftHeading).listen(this, false) { + WebsocketManager.singleton.sendPayload("KeyYawRelativeToAircraftHeading", it.toString()) + } + + // 图传 + KeyTools.createKey(AirLinkKey.KeySignalQuality).listen(this, false) { + it?.let { onChangeAirlinkSignal(it) } + WebsocketManager.singleton.sendPayload("KeySignalQuality", it.toString()) + } + + // 电池 + KeyTools.createKey(BatteryKey.KeyChargeRemainingInPercent).listen(this, false) { + it?.let { onChangeCharger(it) } + WebsocketManager.singleton.sendPayload("KeyChargeRemainingInPercent", it.toString()) + } + } + + companion object { + val singleton: DjiManager by lazy { DjiManager() } + fun registerApp(context: Context) { + SDKManager.getInstance().init(context, object : SDKManagerCallback { + override fun onInitProcess(event: DJISDKInitEvent?, totalProcess: Int) { + if (event == DJISDKInitEvent.INITIALIZE_COMPLETE) { + SDKManager.getInstance().registerApp() + LogManager.i("Init Dji") + } else { + LogManager.i("Can not init Dji") + } + } + override fun onRegisterSuccess() { LogManager.i("Register Dji Success") } + override fun onRegisterFailure(error: IDJIError?) { LogManager.i("Register Dji Fail: $error") } + override fun onProductConnect(productId: Int) { } + override fun onProductDisconnect(productId: Int) { } + override fun onProductChanged(productId: Int) { LogManager.i("Dji product changed: $productId") } + override fun onDatabaseDownloadProgress(current: Long, total: Long) { LogManager.i("Dji DB: ${current / total}") } + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/managers/FileManager.kt b/app/src/main/java/com/power/ops/managers/FileManager.kt new file mode 100644 index 0000000..1bca975 --- /dev/null +++ b/app/src/main/java/com/power/ops/managers/FileManager.kt @@ -0,0 +1,37 @@ +package com.power.ops.managers + +import android.os.Environment +import java.io.File + +class FileManager { + val baseDir = File(Environment.getExternalStoragePublicDirectory("Download"), "PowerOps") + val tempDir = File(baseDir, "temp") + val airlineDir = File(baseDir, "airline") + + init { + recreateDir(baseDir) + recreateDir(tempDir) + recreateDir(airlineDir) + } + + fun recreateDir(dir: File) { + if (!dir.exists()) { dir.mkdir() } + } + + fun recreateFile(file: File) { + if (file.exists()) { file.delete() } + file.createNewFile() + } + + fun read(file: File): String { + return file.readText() + } + + fun write(file: File, content: String) { + file.writeText(content) + } + + companion object { + val singleton: FileManager = FileManager() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/managers/GeckoManager.kt b/app/src/main/java/com/power/ops/managers/GeckoManager.kt new file mode 100644 index 0000000..f41ea7c --- /dev/null +++ b/app/src/main/java/com/power/ops/managers/GeckoManager.kt @@ -0,0 +1,14 @@ +package com.power.ops.managers + +import org.mozilla.geckoview.* + +class GeckoManager { + lateinit var geckoView: GeckoView + lateinit var runtime: GeckoRuntime + + val session = GeckoSession() + + companion object { + val singleton: GeckoManager by lazy { GeckoManager() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/managers/HttpManager.kt b/app/src/main/java/com/power/ops/managers/HttpManager.kt new file mode 100644 index 0000000..d088fcc --- /dev/null +++ b/app/src/main/java/com/power/ops/managers/HttpManager.kt @@ -0,0 +1,144 @@ +package com.power.ops.managers + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.power.ops.utils.ip +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.Request +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody +import okio.IOException +import java.io.File + +enum class Method { POST, GET } + +data class DefaultResponse(val data: Any, val message: String, val code: String) + +data class LoginRes(val creator: Number, val token: String) +data class UploadRes(val file: String) + +class HttpManager { + val baseUrl = "http://$ip" + var token = "" + var creator = 0 + + suspend fun downloadFile(file: File, fullUrl: String) { + FileManager.singleton.recreateFile(file) + val request = Request.Builder().url(fullUrl).get().build() + val deferred = CoroutineScope(Dispatchers.IO).async { + try { + val res = httpClient.newCall(request).execute() + res.body?.let { + file.outputStream().use { output -> + it.byteStream().use { input -> + input.copyTo(output) + } + } + } + } catch (error: IOException) { + LogManager.i("Download io error $error") + } + } + deferred.await() + } + + suspend fun uploadJpg(byteArray: ByteArray): UploadRes? { + val body = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", "file.jpg", byteArray.toRequestBody("image/jpeg".toMediaType())) + .build() + val request = Request.Builder().url("$baseUrl/api/v1/file/files/upload").post(body).build() + val deferred = CoroutineScope(Dispatchers.IO).async { + try { + val res = httpClient.newCall(request).execute() + res.body?.let { + parseDataJson(it.string())?.let { data -> + try { + return@async Gson().fromJson(data, UploadRes::class.java) + } catch (_: JsonSyntaxException) {} + } + } + } catch (error: IOException) { + LogManager.i("Req io error $error") + } + return@async null + } + return deferred.await() + } + + fun parseDataJson(body: String): String? { + try { + val parsed = Gson().fromJson(body, DefaultResponse::class.java) + if (parsed.code == "OK") { + return Gson().toJson(parsed.data) + } + } catch (error: JsonSyntaxException) { + LogManager.i("ParseDataJson error $error") + } + return null + } + + suspend fun req( + method: Method, url: String, jsonString: String = "" + ): String? { + val request: Request = if (method == Method.GET) { + Request.Builder().url(baseUrl + url).get().build() + } else { + val mediaType = "application/json;charset=utf-8".toMediaType() + val body = jsonString.toRequestBody(mediaType) + Request.Builder().url(baseUrl + url).post(body).build() + } + + val deferred = CoroutineScope(Dispatchers.IO).async { + try { + val res = httpClient.newCall(request).execute() + res.body?.let { + return@async parseDataJson(it.string()) + } + } catch (error: IOException) { + LogManager.i("Req io error $error") + } + return@async null + } + return deferred.await() + } + + suspend fun reqSendLogs(logs: ArrayList) { + req(Method.POST, "/api/v1/aircraft/logs", Gson().toJson( + mapOf( "logs" to logs ) + )) + } + + suspend fun reqCreateAirtaskPhoto(name: String, fileId: String) { + req(Method.POST, "/api/v1/aircraft/airtasks/photo", Gson().toJson( + mapOf( + "name" to name, + "fileId" to fileId + ) + )) + } + + suspend fun reqLogin(phone: String, password: String): LoginRes? { + val res = req(Method.POST, "/api/v1/user/login", Gson().toJson( + mapOf( + "phone" to phone, + "password" to password + ) + )) + if (res !== null) { + try { + return Gson().fromJson(res, LoginRes::class.java) + } catch (_: JsonSyntaxException) {} + } + return null + } + + companion object { + val httpClient: OkHttpClient = OkHttpClient() + val singleton: HttpManager by lazy { HttpManager() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/managers/LogManager.kt b/app/src/main/java/com/power/ops/managers/LogManager.kt new file mode 100644 index 0000000..0a408ab --- /dev/null +++ b/app/src/main/java/com/power/ops/managers/LogManager.kt @@ -0,0 +1,17 @@ +package com.power.ops.managers + +import android.util.Log +class LogManager { + companion object { + val logs: ArrayList = arrayListOf() + fun i(message: String) { + logs.add(message) + if (logs.size > 1000) { logs.removeAt(0) } + Log.i("POWER_OPS", message) + } + + fun d(message: String) { + Log.d("POWER_OPS", message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/managers/WebsocketManager.kt b/app/src/main/java/com/power/ops/managers/WebsocketManager.kt new file mode 100644 index 0000000..2cea4e7 --- /dev/null +++ b/app/src/main/java/com/power/ops/managers/WebsocketManager.kt @@ -0,0 +1,66 @@ +package com.power.ops.managers + +import com.google.gson.Gson +import com.power.ops.utils.wsip +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener + +data class Payload(val data: Map) + +class WebsocketManager { + var websocket = websocketClient.newWebSocket( + Request.Builder().url("ws://$wsip:81").build(), + object: WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + LogManager.i("Websocket opened") + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + super.onClosed(webSocket, code, reason) + LogManager.i("Websocket closed") + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + super.onFailure(webSocket, t, response) + LogManager.i("Websocket failure: $t") + } + + override fun onMessage(webSocket: WebSocket, text: String) { + super.onMessage(webSocket, text) + handlePayloads(text) + } + } + ) + + fun handlePayloads(message: String) { + try { + val payload = Gson().fromJson(message, Payload::class.java) + DjiManager.singleton.execute(payload) + } catch (e: Error) { + LogManager.i("Handle payloads: $e") + } + } + + fun send(message: String) { + websocket.send(message) + } + + fun sendPayload(key: String, value: String) { + val payload = Payload(mapOf( key to value )) + send(Gson().toJson(payload)) + } + + fun sendPayloads(map: Map) { + val payload = Payload(map) + send(Gson().toJson(payload)) + } + + companion object { + val websocketClient: OkHttpClient by lazy { OkHttpClient() } + val singleton: WebsocketManager by lazy { WebsocketManager() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/pages/AircraftPage.kt b/app/src/main/java/com/power/ops/pages/AircraftPage.kt new file mode 100644 index 0000000..594a37c --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/AircraftPage.kt @@ -0,0 +1,255 @@ +package com.power.ops.pages + +import android.content.Context +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.view.TextureView +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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Camera +import androidx.compose.material.icons.rounded.Cameraswitch +import androidx.compose.material.icons.rounded.Flight +import androidx.compose.material.icons.rounded.GroupWork +import androidx.compose.material.icons.rounded.PlayCircleOutline +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.icons.rounded.Route +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.zIndex +import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.navigation.NavController +import com.power.ops.components.Dropdown +import com.power.ops.components.DropdownItem +import com.power.ops.components.Header +import com.power.ops.components.HeaderType +import com.power.ops.components.KVText +import com.power.ops.components.Tip +import com.power.ops.managers.DjiManager +import com.power.ops.managers.FileManager +import com.power.ops.managers.HttpManager +import com.power.ops.managers.LogManager +import com.power.ops.managers.WebsocketManager +import com.power.ops.theme.Main400 +import com.power.ops.theme.Main500 +import com.power.ops.theme.Secondary500 +import com.power.ops.theme.Success500 +import com.power.ops.utils.calculateMD5 +import com.power.ops.utils.redirectTo +import com.power.ops.vms.SettingVM +import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + + +@Composable +fun AircraftPage(navController: NavController) { + var ctx: Context? = null + var view: SurfaceView? = null + var holder: SurfaceHolder? = null + + var isShowTip by remember { mutableStateOf(false) } + var tipText by remember { mutableStateOf("成功") } + + var altitude by remember { mutableStateOf(0.0) } + var takeOffAltitude by remember { mutableStateOf(0.0) } + var charger by remember { mutableStateOf(0) } + var airlinkSignal by remember { mutableStateOf(0) } + + var livestreamStatus by remember { mutableStateOf("") } + var channelStatus by remember { mutableStateOf("未获取") } + var airlineUrl by remember { mutableStateOf(".kmz") } + + val coroutineScope = rememberCoroutineScope() + val lifecycle = LocalLifecycleOwner.current.lifecycle + + LaunchedEffect(Unit) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + coroutineScope.launch { + delay(1000) + LogManager.i(calculateMD5("http://nofee-default-bucket.oss-cn-guangzhou.aliyuncs.com/files/0acd8cf3-af61-4d6c-bebd-c21a1708dac0.kmz?OSSAccessKeyId=LTAI5tQ3dzfKXUnqW6XA6Yvb&Expires=1702017543&Signature=jPmkiXaH3xP2hKtAsnkWv6b0T0A%3D")) + if (ctx != null && holder != null && view != null) { + DjiManager.singleton.loadVideoSurface(ctx!!, holder!!, view!!) + } + DjiManager.singleton.reportKeys( + onChangeAltitude = { altitude = it }, + onChangeTakeOffAltitude = { takeOffAltitude = it }, + onChangeCharger = { charger = it }, + onChangeAirlinkSignal = { airlinkSignal = it } + ) + } + } + } + lifecycle.addObserver(observer) + } + + Column { + Header( + navController = navController, + headerType = HeaderType.MAIN, + title = "新能源飞控中心", + height = 35.dp, + fontSize = 16.sp + ) + + Row( + modifier = Modifier + .weight(1f) + .background(color = Secondary500)) + { + Column(modifier = Modifier + .background(Main400) + .width(55.dp) + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceAround + ) { + Dropdown(imageVector = Icons.Rounded.Camera, items = arrayOf( + DropdownItem("拍照") { + DjiManager.singleton.takePhoto() + }, + DropdownItem("开启媒体") { + DjiManager.singleton.enableMedia() + }, + DropdownItem("关闭媒体") { + DjiManager.singleton.disableMedia() + } + )) + + Dropdown(imageVector = Icons.Rounded.Cameraswitch, items = arrayOf( + DropdownItem("广角") { + DjiManager.singleton.changeCameraType(CameraVideoStreamSourceType.WIDE_CAMERA) + }, + DropdownItem("短焦") { + DjiManager.singleton.changeCameraType(CameraVideoStreamSourceType.ZOOM_CAMERA) + DjiManager.singleton.changeCameraZoom(2.0) + }, + DropdownItem("中焦") { + DjiManager.singleton.changeCameraType(CameraVideoStreamSourceType.ZOOM_CAMERA) + DjiManager.singleton.changeCameraZoom(4.0) + }, + DropdownItem("长焦") { + DjiManager.singleton.changeCameraType(CameraVideoStreamSourceType.ZOOM_CAMERA) + DjiManager.singleton.changeCameraZoom(6.0) + }, + DropdownItem("红外") { + DjiManager.singleton.changeCameraType(CameraVideoStreamSourceType.INFRARED_CAMERA) + } + )) + + Dropdown(imageVector = Icons.Rounded.Flight, items = arrayOf( + DropdownItem("起飞") { + DjiManager.singleton.changeFlyStatus("START_FLY") + }, + DropdownItem("返航") { + DjiManager.singleton.changeFlyStatus("GO_HOME") + }, + DropdownItem("降落") { + DjiManager.singleton.changeFlyStatus("TAKE_OFF") + } + )) + + Dropdown(imageVector = Icons.Rounded.Refresh, items = arrayOf( + DropdownItem("重置实时通讯") { + WebsocketManager.singleton.sendPayload("KeyIsFlying", "false") + DjiManager.singleton.reportKeys( + onChangeAltitude = { altitude = it }, + onChangeTakeOffAltitude = { takeOffAltitude = it }, + onChangeCharger = { charger = it }, + onChangeAirlinkSignal = { airlinkSignal = it } + ) + }, + + DropdownItem("重置流通道") { + channelStatus = DjiManager.singleton.getChannelStatus() + if (ctx != null && holder != null && view != null) { + DjiManager.singleton.loadVideoSurface(ctx!!, holder!!, view!!) + } + }, + + DropdownItem("重置拍摄格式") { + DjiManager.singleton.initPhotoFormat() + }, + + DropdownItem("重置相机内存") { + DjiManager.singleton.clearCamera() + }, + + DropdownItem("上报日志") { + coroutineScope.launch { + HttpManager.singleton.reqSendLogs(LogManager.logs) + } + }, + )) + + } + + Row(modifier = Modifier + .background(Success500) + .weight(1f) + .fillMaxHeight() + .zIndex(-999f) + ) { + AndroidView(factory = { context -> + ctx = context + SurfaceView(context).apply { + holder = this.holder + view = this + } + }, modifier = Modifier + .fillMaxSize() + .zIndex(-99f)) + + Tip(showTip = isShowTip, text = tipText) { isShowTip = false } + } + Column(modifier = Modifier + .background(Main400) + .width(120.dp) + .fillMaxHeight()) { + KVText(key = "图传", value = "$airlinkSignal") + KVText(key = "直播", value = livestreamStatus) + KVText(key = "通道", value = channelStatus) + KVText(key = "链接", value = airlineUrl) + KVText(key = "电量", value = "$charger") + KVText(key = "基准", value = "$takeOffAltitude") + KVText(key = "高度", value = "$altitude") + } + } + +// Row( +// modifier = Modifier +// .fillMaxWidth() +// .padding(horizontal = 10.dp), +// verticalAlignment = Alignment.CenterVertically, +// horizontalArrangement = Arrangement.SpaceBetween +// ) { +// +// +// } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/pages/AircraftSettingPage.kt b/app/src/main/java/com/power/ops/pages/AircraftSettingPage.kt new file mode 100644 index 0000000..fae5bc4 --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/AircraftSettingPage.kt @@ -0,0 +1,113 @@ +package com.power.ops.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.power.ops.components.Header +import com.power.ops.components.HeaderType +import com.power.ops.theme.Char700 +import com.power.ops.theme.Main400 +import com.power.ops.theme.PowerTheme +import com.power.ops.theme.Secondary500 +import com.power.ops.theme.Typography +import com.power.ops.vms.SettingVM +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AircraftSettingPage(navController: NavController) { + + var agoraChannelId by remember { mutableStateOf("") } + var agoraToken by remember { mutableStateOf("") } + var agoraUid by remember { mutableStateOf("") } + + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(coroutineScope) { + val channelId = SettingVM.singleton.getSetting("agoraChannelId") + val token = SettingVM.singleton.getSetting("agoraToken") + val uid = SettingVM.singleton.getSetting("agoraUid") + + if (channelId != null) { agoraChannelId = channelId.value } + if (token != null) { agoraToken = token.value } + if (uid != null) { agoraUid = uid.value } + } + + Column(modifier = Modifier + .fillMaxSize() + .background(color = Secondary500)) { + Header( + navController, + HeaderType.CONFIRM, + title = "飞控设置", + ) { + coroutineScope.launch { + SettingVM.singleton.setSetting("agoraChannelId", agoraChannelId) + SettingVM.singleton.setSetting("agoraToken", agoraToken) + SettingVM.singleton.setSetting("agoraUid", agoraUid) + navController.popBackStack() + } + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(14.dp) + ) { + TextField( + value = agoraChannelId, + onValueChange = { agoraChannelId = it }, + label = { Text("通道 ID", color = Char700, style = Typography.labelSmall) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 7.dp), + colors = TextFieldDefaults.textFieldColors(containerColor = Main400) + ) + + TextField( + value = agoraToken, + onValueChange = { agoraToken = it }, + label = { Text("Token", color = Char700, style = Typography.labelSmall) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 7.dp), + colors = TextFieldDefaults.textFieldColors(containerColor = Main400) + ) + + TextField( + value = agoraUid, + onValueChange = { agoraUid = it }, + label = { Text("UID", color = Char700, style = Typography.labelSmall) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 7.dp), + colors = TextFieldDefaults.textFieldColors(containerColor = Main400) + ) + } + } +} + +@Preview +@Composable +fun PreviewAircraftSettingPage() { + PowerTheme { + AircraftSettingPage(navController = NavController(LocalContext.current)) + } +} diff --git a/app/src/main/java/com/power/ops/pages/HomePage.kt b/app/src/main/java/com/power/ops/pages/HomePage.kt new file mode 100644 index 0000000..c672b02 --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/HomePage.kt @@ -0,0 +1,43 @@ +package com.power.ops.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import com.power.ops.components.BottomBar +import com.power.ops.components.GeckoWebview +import com.power.ops.components.Header +import com.power.ops.components.HeaderType +import com.power.ops.components.Webview +import com.power.ops.theme.PowerTheme +import com.power.ops.theme.Secondary500 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomePage(navController: NavController) { + Column { + Header(navController = navController,headerType = HeaderType.MAIN, title = "首页") + Column( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .background(color = Secondary500) + ) { + GeckoWebview() + } + BottomBar(navController, "HomePage") + } +} + +@Preview +@Composable +fun PreviewHomePage() { + PowerTheme { + HomePage(navController = NavController(LocalContext.current)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/pages/LoginPage.kt b/app/src/main/java/com/power/ops/pages/LoginPage.kt new file mode 100644 index 0000000..fc55aab --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/LoginPage.kt @@ -0,0 +1,137 @@ +package com.power.ops.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.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 androidx.navigation.NavController +import com.power.ops.components.Tip +import com.power.ops.managers.HttpManager +import com.power.ops.theme.Char400 +import com.power.ops.theme.Char500 +import com.power.ops.theme.Char600 +import com.power.ops.theme.Main400 +import com.power.ops.theme.Main500 +import com.power.ops.theme.PowerTheme +import com.power.ops.theme.Primary500 +import com.power.ops.theme.Secondary500 +import com.power.ops.theme.Typography +import com.power.ops.utils.openUrl +import com.power.ops.vms.SettingVM +import kotlinx.coroutines.launch + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginPage(navController: NavController) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + var phone by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + var isShowTip by remember { mutableStateOf(false) } + var tipText by remember { mutableStateOf("成功") } + + LaunchedEffect(coroutineScope) { + SettingVM.singleton.setSetting("token", "") + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(color = Secondary500), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "新能源智能", style = Typography.headlineLarge) + Text(text = "Welcome to Power Ops System", color = Char600, modifier = Modifier.padding(4.dp), style = Typography.labelSmall) + + OutlinedTextField( + value = phone, + onValueChange = { phone = it }, + placeholder = { Text(text = "账号") }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, start = 30.dp, end = 30.dp), + colors = TextFieldDefaults.outlinedTextFieldColors(unfocusedBorderColor = Char400, focusedBorderColor = Primary500) + ) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + placeholder = { Text(text = "密码") }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 14.dp, start = 30.dp, end = 30.dp), + colors = TextFieldDefaults.outlinedTextFieldColors(unfocusedBorderColor = Char400, focusedBorderColor = Primary500) + ) + + Button( + onClick = { + coroutineScope.launch { + val res = HttpManager.singleton.reqLogin(phone, password) + if (res != null) { + HttpManager.singleton.creator = res.creator.toInt() + HttpManager.singleton.token = res.token + SettingVM.singleton.setSetting("token", res.token) + navController.navigate("HomePage") + } else { + tipText = "登录出错" + isShowTip = true + } + } + }, + shape = RoundedCornerShape(5.dp), + modifier = Modifier + .fillMaxWidth() + .height(65.dp) + .padding(top = 14.dp, start = 30.dp, end = 30.dp), + colors = ButtonDefaults.buttonColors(containerColor = Primary500, contentColor = Main500)) { + Text("立即登录", color = Main500) + } + + Button( + onClick = { openUrl(context, "http://power.nofee.fun") }, + shape = RoundedCornerShape(5.dp), + modifier = Modifier + .fillMaxWidth() + .height(65.dp) + .padding(top = 14.dp, start = 30.dp, end = 30.dp), + colors = ButtonDefaults.buttonColors(containerColor = Main400, contentColor = Char500)) { + Text("前往注册") + } + + Tip(showTip = isShowTip, text = tipText) { isShowTip = false } + } +} + +@Preview +@Composable +fun PreviewLoginPage() { + PowerTheme { + LoginPage(navController = NavController(LocalContext.current)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/pages/MessagePage.kt b/app/src/main/java/com/power/ops/pages/MessagePage.kt new file mode 100644 index 0000000..45ba05f --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/MessagePage.kt @@ -0,0 +1,28 @@ +package com.power.ops.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import com.power.ops.components.BottomBar +import com.power.ops.components.Webview +import com.power.ops.theme.Secondary500 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MessagePage(navController: NavController) { + Column { + Column( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .background(color = Secondary500) + ) { + Webview(uri = "/message") + } + BottomBar(navController, "MessagePage") + } +} diff --git a/app/src/main/java/com/power/ops/pages/SettingPage.kt b/app/src/main/java/com/power/ops/pages/SettingPage.kt new file mode 100644 index 0000000..f04ca72 --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/SettingPage.kt @@ -0,0 +1,160 @@ +package com.power.ops.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AccountCircle +import androidx.compose.material.icons.rounded.AirplanemodeActive +import androidx.compose.material.icons.rounded.ListAlt +import androidx.compose.material.icons.rounded.Logout +import androidx.compose.material.icons.rounded.NetworkCheck +import androidx.compose.material.icons.rounded.PrivacyTip +import androidx.compose.material.icons.rounded.Upload +import androidx.compose.material3.ExperimentalMaterial3Api +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.power.ops.components.BottomBar +import com.power.ops.components.Dialog +import com.power.ops.components.Divider +import com.power.ops.components.Header +import com.power.ops.components.HeaderType +import com.power.ops.components.RichItem +import com.power.ops.components.Tip +import com.power.ops.managers.HttpManager +import com.power.ops.managers.LogManager +import com.power.ops.theme.Main400 +import com.power.ops.theme.PowerTheme +import com.power.ops.theme.Secondary500 +import com.power.ops.theme.Typography +import com.power.ops.utils.getVersionName +import com.power.ops.utils.openUrl +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingPage(navController: NavController) { + var isShowDialog by remember { mutableStateOf(false) } + + var isShowTip by remember { mutableStateOf(false) } + var tipText by remember { mutableStateOf("成功") } + + + //val settingDao: SettingDao = SettingDatabase.getDatabase(LocalContext.current).settingDao() + // SettingVM.singleton(LocalContext.current).setSetting() + // val settings by settingDao.querySettings().collectAsState(initial = emptyList()) + // var settings by object : SettingVM() + + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + Column(modifier = Modifier + .fillMaxHeight() + .background(color = Secondary500)) { + Header( + navController = navController, + headerType = HeaderType.CONFIRM, + title = "设置" + ) { + navController.popBackStack() + } + LazyColumn( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + ) { + this.item { + Column(modifier = Modifier + .padding(start = 14.dp, end = 14.dp, top = 14.dp) + .clip(shape = RoundedCornerShape(10.dp)) + .background(Main400) + ) { + RichItem(imageVector = Icons.Rounded.AccountCircle, text = "修改资料") { + tipText = "未开放" + isShowTip = true + } + Divider() + RichItem(imageVector = Icons.Rounded.NetworkCheck, text = "网络设置") { + tipText = "开发中" + isShowTip = true + } + Dialog(defaultValue = "http://", showDialog = isShowDialog, onDismiss = { + isShowDialog = false + }, onConfirm = { + LogManager.i(it) + }) + Divider() + RichItem(imageVector = Icons.Rounded.AirplanemodeActive, text = "飞控设置") { + navController.navigate("AircraftSettingPage") + } + } + + Column(modifier = Modifier + .padding(start = 14.dp, end = 14.dp, top = 14.dp) + .clip(shape = RoundedCornerShape(10.dp)) + .background(Main400) + ) { + RichItem(imageVector = Icons.Rounded.ListAlt, text = "上传日志") { + coroutineScope.launch { + tipText = "上传成功" + HttpManager.singleton.reqSendLogs(LogManager.logs) + isShowTip = true + } + } + Divider() + RichItem(imageVector = Icons.Rounded.PrivacyTip, text = "隐私政策") { + openUrl(context, "http://power.nofee.fun/privacy") + } + Divider() + RichItem(imageVector = Icons.Rounded.Upload, text = "版本更新", intro = "内测版:${getVersionName(context)}") { + openUrl(context, "http://power.nofee.fun/android") + } + } + + Column(modifier = Modifier + .padding(start = 14.dp, end = 14.dp, top = 14.dp) + .clip(shape = RoundedCornerShape(10.dp)) + .background(Main400) + ) { + RichItem(imageVector = Icons.Rounded.Logout, text = "退出登录") { + navController.navigate("LoginPage") + } + } + + Text( + text = "© 2023 新能源智能", + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + textAlign = TextAlign.Center, + style = Typography.bodySmall + ) + } + } + Tip(showTip = isShowTip, text = tipText) { isShowTip = false } + } +} + +@Preview +@Composable +fun PreviewSettingPage() { + PowerTheme { + SettingPage(navController = NavController(LocalContext.current)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/pages/SitePage.kt b/app/src/main/java/com/power/ops/pages/SitePage.kt new file mode 100644 index 0000000..cf96858 --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/SitePage.kt @@ -0,0 +1,28 @@ +package com.power.ops.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import com.power.ops.components.BottomBar +import com.power.ops.components.Webview +import com.power.ops.theme.Secondary500 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SitePage(navController: NavController) { + Column { + Column( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .background(color = Secondary500) + ) { + Webview(uri = "/developer") + } + BottomBar(navController, "SitePage") + } +} diff --git a/app/src/main/java/com/power/ops/pages/SubmitPage.kt b/app/src/main/java/com/power/ops/pages/SubmitPage.kt new file mode 100644 index 0000000..0d207a5 --- /dev/null +++ b/app/src/main/java/com/power/ops/pages/SubmitPage.kt @@ -0,0 +1,26 @@ +package com.power.ops.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import com.power.ops.components.BottomBar +import com.power.ops.components.Webview +import com.power.ops.theme.Secondary500 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SubmitPage(navController: NavController) { + Column { + Column( + modifier = Modifier.fillMaxHeight() + .weight(1f).background(color = Secondary500) + ) { + Webview(uri = "/workflow") + } + BottomBar(navController, "SubmitPage") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/theme/Color.kt b/app/src/main/java/com/power/ops/theme/Color.kt new file mode 100644 index 0000000..0f7277f --- /dev/null +++ b/app/src/main/java/com/power/ops/theme/Color.kt @@ -0,0 +1,51 @@ +package com.power.ops.theme + +import androidx.compose.ui.graphics.Color + +val Main300 = Color(0xFFD0BCFF) +val Main400 = Color(0xFF062e5f) +val Main500 = Color(0xFF00224c) +val Main600 = Color(0xFF011835) +val Main700 = Color(0xFF010a15) + +val Secondary300 = Color(0xFF004684) +val Secondary400 = Color(0xFF003e73) +val Secondary500 = Color(0xFF003767) +val Secondary600 = Color(0xFF02335d) +val Secondary700 = Color(0xFF052d4f) + +val Primary300 = Color(0xFF9dfdff) +val Primary400 = Color(0xFF52ecef) +val Primary500 = Color(0xFF00d3d8) +val Primary600 = Color(0xFF02bcc0) +val Primary700 = Color(0xFF03888b) + +val Error300 = Color(0xFF936f82) +val Error400 = Color(0xFFdb696d) +val Error500 = Color(0xFFfa3f3f) +val Error600 = Color(0xFFcf2f33) +val Error700 = Color(0xFFa70c14) + +val Warning300 = Color(0xFF919681) +val Warning400 = Color(0xFFdbb36d) +val Warning500 = Color(0xFFf9b630) +val Warning600 = Color(0xFFcf9633) +val Warning700 = Color(0xFFaf7815) + +val Success300 = Color(0xFF59a79c) +val Success400 = Color(0xFF62e0a4) +val Success500 = Color(0xFF17d872) +val Success600 = Color(0xFF24cc79) +val Success700 = Color(0xFF02a85b) + +val Info300 = Color(0xFF5386b8) +val Info400 = Color(0xFF609edf) +val Info500 = Color(0xFF1c86f7) +val Info600 = Color(0xFF186dc7) +val Info700 = Color(0xFF0252a9) + +val Char300 = Color(0xFFf1f5f9) +val Char400 = Color(0xFFe2e8f0) +val Char500 = Color(0xFFcbd5e1) +val Char600 = Color(0xFF94a3b8) +val Char700 = Color(0xFF64748b) \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/theme/Theme.kt b/app/src/main/java/com/power/ops/theme/Theme.kt new file mode 100644 index 0000000..e0dfd55 --- /dev/null +++ b/app/src/main/java/com/power/ops/theme/Theme.kt @@ -0,0 +1,100 @@ +package com.power.ops.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + displayLarge = TextStyle(color = Char500), + displayMedium = TextStyle(color = Char500), + displaySmall = TextStyle(color = Char500), + bodyLarge = TextStyle( + color = Char500, + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + bodyMedium = TextStyle(color = Char500), + bodySmall = TextStyle( + color = Char700, + fontSize = 10.sp + ), + titleLarge = TextStyle( + color = Char400, + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + titleMedium = TextStyle(color = Char500), + titleSmall = TextStyle(color = Char500), + labelLarge = TextStyle(color = Char500), + labelMedium = TextStyle(color = Char500), + labelSmall = TextStyle(color = Char500), + headlineLarge = TextStyle( + color = Char500, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 30.sp, + ), + headlineMedium = TextStyle(color = Char500), + headlineSmall = TextStyle(color = Char500) +) + +private val DarkColorScheme = darkColorScheme( + primary = Primary500, + secondary = Secondary500, + tertiary = Secondary400 +) + +private val LightColorScheme = lightColorScheme() + +@Composable +fun PowerTheme( + content: @Composable () -> Unit +) { + val darkTheme = true + val dynamicColor = false + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = Main500.toArgb() + + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/utils/Helper.kt b/app/src/main/java/com/power/ops/utils/Helper.kt new file mode 100644 index 0000000..eaba252 --- /dev/null +++ b/app/src/main/java/com/power/ops/utils/Helper.kt @@ -0,0 +1,51 @@ +package com.power.ops.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.navigation.NavController +import androidx.navigation.NavOptions +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 +import java.io.ByteArrayOutputStream +import java.security.MessageDigest + +fun calculateMD5(input: String): String { + val md = MessageDigest.getInstance("MD5") + val byteArray = md.digest(input.toByteArray()) + + val result = StringBuilder() + for (byte in byteArray) { + result.append(String.format("%02x", byte)) + } + + return result.toString() +} + +fun bitmapToBase64(bitmap: Bitmap): String { + val byteArrayOutputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) + val byteArray = byteArrayOutputStream.toByteArray() + return Base64.encodeToString(byteArray, Base64.DEFAULT) +} + +val wsip = "8.134.154.108" +// val ip = "8.134.154.108" +val ip = "power.nofee.fun" +// val ip = "172.20.10.9" +// val ip = "192.168.31.178" + +fun redirectTo(navController: NavController, route: String) { + val navOptions = NavOptions.Builder().setPopUpTo(navController.graph.startDestinationRoute, inclusive = true).build() + navController.navigate(route, navOptions = navOptions) +} + +fun openUrl(context: Context, url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) +} + +fun getVersionName(context: Context): String { + return context.packageManager.getPackageInfo(context.packageName, 0).versionName +} \ No newline at end of file diff --git a/app/src/main/java/com/power/ops/vms/SettingVM.kt b/app/src/main/java/com/power/ops/vms/SettingVM.kt new file mode 100644 index 0000000..1a808b9 --- /dev/null +++ b/app/src/main/java/com/power/ops/vms/SettingVM.kt @@ -0,0 +1,99 @@ +package com.power.ops.vms + +import android.app.Application +import android.content.Context +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.Update +import com.power.ops.managers.FileManager +import com.power.ops.managers.LogManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + + +@Entity(tableName = "settings") +data class SettingEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + var key: String, + var value: String +) + +@Dao +interface SettingDao { + @Query("select * from settings") + fun querySettings(): Flow> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun createSetting(setting: SettingEntity) + + @Update + suspend fun updateSetting(setting: SettingEntity) +} + +@Database(entities = [SettingEntity::class], version = 1) +abstract class SettingDatabase : RoomDatabase() { + abstract fun settingDao(): SettingDao + companion object { + @Volatile + private var INSTANCE: SettingDatabase? = null + fun getDatabase(context: Context): SettingDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + SettingDatabase::class.java, + "setting" + ).build() + INSTANCE = instance + instance + } + } + } +} + +class SettingVM { + lateinit var dao: SettingDao + lateinit var settings:Flow> + suspend fun getSetting(key: String): SettingEntity? { + settings = dao.querySettings() + val res = settings.map { + it.find { setting -> + setting.key == key + } + }.first() + return res + } + + suspend fun setSetting(key: String, value: String) { + val res = getSetting(key) + if (res == null) { + dao.createSetting(SettingEntity(key = key, value = value)) + } else { + res.value = value + dao.updateSetting(res) + } + } + + fun initDao(context: Context) { + dao = SettingDatabase.getDatabase(context).settingDao() + settings = dao.querySettings() + } + + companion object { + val singleton: SettingVM = SettingVM() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..ad4892a --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/i3060.xml b/app/src/main/res/drawable/i3060.xml new file mode 100644 index 0000000..16ec0b8 --- /dev/null +++ b/app/src/main/res/drawable/i3060.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..7353dbd --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..a52e8fc93ddbe476bd834035e85cfa3b21c3cc90 GIT binary patch literal 1426 zcmV;D1#S9LNk&GB1pok7MM6+kP&iC|1pojqN5Byf2?lK&NmAPV?#dtW6huV-C&2Wj zlt==tBIT$iCj%Q7@CY+HG%u7!lBBFW^$d5<`G@$HVur5D^oAHLs9C4OQDe#ma9MZJK$w19uXc#I10LCnJ&O?(R0z zW_b3G#UCDLBhPWIXA@mOl1TqI2i)4WRaNT=ZlR^L;6zq}mvif&2^t`?klbA|m#_C8 zx3O)j%DE4BrwJ!?h#4a516rXCcejkpp$S5__uiiXI@q=ylb-R+`nPS{Mr^0bk(yOX zceYA9ty9^y_1orf@on_YM%@*@(H%a~7u%_vG$A{-ZPT`TA9LgCS&8ufrTT)ib+p!u z2>^(1?(rC<5|N+;ptylzfTT=-xfM9%oCTtam<(GQLZMPDMKR2NU=8XKz$r0;zhlm~Z1X8FNOKg|bAd1z5vUUU^Isber49MlJw@nO_>X=Z%0OB?X2kzjw z9M6hC0pOG=$ZIVbKRXb%C;~R98Q5&Q;K~mGYhqi9A(er5RSVt(2fgrF3%a)=-Dlm=-5f+r5JI*fONUJ(+)nVCY}1zfc_rma-b5kGaG z$#d5atxtiIgmWfI_Ckliw+6IpLg15|Up17aXu?g>qZyA8Y=n z*~vZ?CgBw;U6n3}RM+F?ZYxL{$4C7cd=4ZekjSvK?#^@ymfLEE)j(rkXetheUdgzy@o{r7Ih-uZioWQ}72y4&)c>gW*5K=eKp2!} z8QF?5@x?`6rKiH`7&!gH_4q3*-shGil4yTz6X*GLi$Gsyql4MCC%Yg!)?+bM)C? z)c`20 ztiYYyo%_7Ci3I`N=RU8G-De62uSo{1brg`mG>_c&etYS%c2K`$KsIOJ)dC`bI8(=n zUrql1DVgxDW(7!%(u+P7@A5sQPJ{dKwV z+=75<1qet})41{6@yv;A5|jXxIlbA9*REbDqS%}g4WqtxKhEp-qFG&T6G0Qed)c_s zE>mp~2?ksU(Ef4lc3Lwj0}m)uWJAJ72OB5g?WmS*Jmw3ydc%?dh@=JrfSyVEq)*Z_ zNuMO?lk`l|Cw-EhN&2M!Q-o>ybKUc@@?Z#DVWFWTK#(%P$qHBiZOpJozz?`V{1Lm5 gfTmg6^x#)*Xcjufku|*h*yag5`Is@oAMp(p0DQU6YXATM literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..0cd19de034f1d859b2da9655edfb905b269c948e GIT binary patch literal 3214 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 A^8f$< literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..a0ac705a62cf6b13d4f002a3eeb456b1cfea3f51 GIT binary patch literal 956 zcmV;t14H~$Nk&Gr0{{S5MM6+kP&iDd0{{RoFTe{BzXvIjBu7$2Rj+<8e*pVFEC)oZ z?py^TAV-p|L$D1ZQC$T8QsnR z5FkQ<01*-_P9H=4_ppVq8G3X&?VgTf8W>e8QQMEdP^eNtAqS(OF%kO4 zVAI&yfL_aDAkZ15gQ*Vc`|#EK(RR>Db?VeYNQk{gXxW&QC~<2b6c7ri@W>;O5EP1q zvaqmD|45l7rx|jK`gB;FlA$D+1OX)oQi33;B1)Ye-V}Q3K%hqvO57YjOg}Lx@l?&* zDrGh+ygdA0{!Tf({*=$JNvglD0C%{jTb{pIF*o^Je9o%#E`?}`-{UZ9$$ZaG=uE85E z`~f*jjK$b7I%Wn$AZBp11Oz2zrI;zB2+R&@#gCeA*r|aEWVZL6!;r{N7h6Us@5%IX zbH$7saJsicMyq1z(1URW>`+eE)~MK4+iF5uIY6pu>E!wiWeJ=4Y2PfulP_8H!&DL` z4YC?Cees(tO21CTH|OwpX3EmHFE3yBw$N`HgKP95&!TNlW4-(y-U-uZQJQ>k45oq; z=c9Nj@e#g(cb_Y?nOSnrd^{tMU6G3UY4MTX0r#IvRBDalBzH2FxmT1R6=WpFdig&H z5X+S+xWFxBajej)l`;`8CHBed4IVs!SSs@fP4VFBz)Pz)C`mM9)E}WT- ziv<|8#XBw`jVF*m?A6ozSrYHjsI5_{Jc0EgQ6z1jF@eN;H)LhBP1R^Kvvixuyr=U% zN~|@D>h=sFqVK}vr$Q8#n!TQ7h!|IJgbPPkZCo>g*mr{+IG9q_3BQy7?>pa~RMWzi z5!zI7i2SFzHAe>9|6uK3?i>Pf|6Rw7Y}IP`r(>7SfE5az{x+!P;Ky92exqhhDQ)u4 eX-X+Hu3yLDkKa_c9(KTFMh;`B#10@Q2><|iozD&c literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..e06fbe9abe2202eccedf14411f538ec09b05e7e7 GIT binary patch literal 1918 zcmV-^2Z8ufNk&F?2LJ$9MM6+kP&iC!2LJ#sFTe{B<}i@9P2*qoeh(2b0sc^Rz+~lQ z|G!9%EM{hAW@Zjm%*@P8%goHXl6XBt!c>tk4njUQ-8o2iZ9I^Kf0Z)nR>C|{kZL7d zckNV>FbY}?+g5j%NL z{}s4xqqN;I;~%J2+wRzIe&vXA&Nt_KX3pNq0f|XSTuh?kk$SlTBlqF%tUL41Ob1^X zUP@DdRe^IJXB(Y^%yM=E*-YHaP2?6_fY~s2khc?3=)(mlKpW+NV-cAeZ+^MBBl=q~ zKCZy%qRX~Z1#!p-K(%dK*|PJW7$mA>cXxOD+^(H{@nrSSXkw5wIaHp2_kt_gwrT6~ z*|z;68<}m}R%Y`9Y`@sHnP0`vHujzj+_sG)wcUMh?%se%37D$|Y!A69LZ*3Ykx)`+ zEmP;9$7K#=Jk(U2;VV}iMUUo4;xR?cG@quir)+Oq6M^P$6h3|X^*QGrec=9NBl*_- zYI{ip^yghc-RzH@wuAtoQOhNM#5MVkx%cjFg6A$Fb|zH9y?J; zH2;?gXYe#`e;i2TRE?z?|0UDpUt+j7lQ-_(TB57ME+<7zVp%Ih6!t`wBY?t5m-S0* z`!iA8lx2$r6zO*TnItgl7oX|1uwe`|DREh%Dv~LlC<2cvhhd(#&pN&#F_vcc-H#}_ z&UA|V?~=PB53i)~SPF?|V3U{aziXU>u2_gqpAxWeBsfoDj>~E>$mvrHivv`@dEQk& z_Z=uH3t<9752Y003G=)c*vC>Q2r!SwVuwiX6pd1R0DzKpEOXD_c@)3Sl3dTgz-N;j zvxisKxWhUR5RnD;u--kkc*U_kas2OMBH}gxUMh?y4mzY6FtC6kE8OzBwe5eSXG@in zyYA^32eU8@-GEne2N=z^x6(OR*(KHbORy=rC{vAZl7+|}-T1l7+wpbag$n~h#-nV) zKood2X9AP8%fI0>Z_K#|=d*7ucC3-+@RBiwl<9uvnJ0C2{-2A~TBC`eL|{Jv3;VBwS6Yo2%n706$ zt(x+XUR+48rW8m4O-m6)2s8U=;I?mZ0z2LxmOBAYohfUe)vTT(H(|=h%@&Y2HG7WI z(ag1^dd3@8sG6ZdI%49P+rDGGVWEAnc=T5?52NWuJ7QVcUYT6O04MmzDi;W$2-Q5THdv_m%K%3( zLZhxn^Cc>OzwCO(aGQ|;{AYwKUEp#i2fF2g;TjAAL1PFcTC7zng5CTevRuEu-P`M( zOGMOQ?6%`FvxiyxhH8Vwx?fE&#|Sh_it~uYS;NC88$Uy#(1wrk-7RMuPcnL3zhj-E zO9CB`fE|q0$}D4{KJ(-FSy7drnL{2=YVWk0-@$p*&kX%h1mZamECdQomRhPeB+cO) z$}Aeqb^K(QoF`xtXtmm$JfL}!C92O=Tdd7oeXHgAkr>lY(HoE6!fk7|HbY=i=>zZI3V}iWg$|GYmK)-X^cFWriTbWT-}ynf zatjH>uAZVRu0wpj&D~J&kzdP-8{D=I;rV2p30FUPN1T4VRUk&7NMKoirm;)P-zLtL zc@pl%PjXJSeNcb8iNNYrV`Kg&R0uSNsM*w=s%#hRc&g`Z2gC)G167ZDZWr#n_QW@p z9C5Wdfh2)Cf%!Ns0rh8UT5Qa8ijKSGf|ByeqweoHrKAvaijS|n?;wH2-yh~LCHr&v zj6m*h0)st6)a*uU@*1Pr`>~&Y+4I5Q4}NBjAfdLeRUl?Q`;b7IK=$=BKK#t&ebEO2 E0CJwH`v3p{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..b920a21309caea09ff539c5455647304a65fc2ba GIT binary patch literal 2038 zcmV6#yCmAGeU0E|^BP?c_*QPCx%&3jJ4te3B%2{1yfw`ab~> z_$!1W1P-VyU>Hh~V6-Z}3>b!?wG0RP$mj)zf?{Q$vIjJ@J@D!&Cul=K+cr$fA9lNo zAR;D!b+ue?$)V3%xzV;KlJu#UnKRf7CV@?&qwHALT#K2RhN|_O($+6)tZy0buFbQU zOda!8scqYaVaop(Yk*tZwrXYVd%xiBPW*DL54xa;TMF4Pf`~!tg4RyJFiFawSPpBr7tz_n1L?q%kx6 z^V!q*{RO`#k#@{5Gus&x0Dz6vkSGNLkSQ1lr;tjLfT#X1U*4vW-o{*}0LMcgsu@*Q zNgHEUCdybq{R|YpY&wLeRFZ((^HAJoItL^|4F(awQB@HQ_8=}bivyDe#T@a$UY=c) zeRlx!Kr6w5>cNGED}XLRwpqEJzdYPtRDG0!7eFn-#s~z4KEQ*Vz)*~6&)Ypme6Sm7 zDfb|30c8l(56CCN(g?7|BG+329-lb9|FYyN5=v-5mkgsp0IRz0(W0AC0_2i9m+Z7y z3u?Hs00U_`K#(=vmm{B9;OngmnG4hn@}4uG2_kEpx9Jvi1|SAtNWmtkO2iv|F z0TIFBMJ>es<2Wv2%#w+LQ04jk)HqMEM4fJB#`UROJ1VP{WhmwH;MaTFuK`e_r;~wz}1KbHCD*CyqaU+j?9Hpj;Rf45GImZ#`%XP~*`P(Lku6 zm45xECwJ&^+xR1?QFtp9#!7SU({rf@&pOKNpWMgpZ{TKJe$~H$LW}6qkW47JTsw~rn5->Dw#!<<5-7nDT=Y?sx7IRd5LVGp z%Yk*@iE4ZNun)7xLgAo5VJ>*5Y0MB@q6a7IQ3hZ-4z2r4RM|DNcTW{4P&k76!;?2O zWtA3)9`%harSY7RBkKOO*1*!K0Ph;m4s*5Byu>!QL^ zs6e53@XOVoH5D`g2J0$6yAKe0z!!&>W8B+T*|AiErvkfO`1Yxvns~zGxZIEE^q-jv zUT}?ebdrj?MExOjIT%BF|C~3OJypGE(anBTw$A>2{r}GiUjV){}6hVKi|GB(mAP5thZuCRxOAqg) zW@e#b);`((DG8;{x=9X!K-A^_^2^c-feOu?MTS}W%=CaM&2^+9C`+XmfmCa5%-~W# z{qWS?vIz`Y#_}SdBgdZ8f8i53suK6@7J8i zqD9aG@OC?8AwXpr-WMR*P2Wv(QHew(03F;)6A03!G=HOLwAEbLU9Ovsay{%D-4}v2 zPA#)iPHoB%7}T1FzFp^&0$)l`2;eV&iwdwaEFWI|3$Pav5flIdc*tCfQD?0KHkN^W zOCbRT!-_`OxMd3d*lo3R!0xo6Qm_w2V#LhpaZ}}v8H5WEN^0`wsH0Isoq!dhFbFY( zkdvs%yJh|=sT1H`SW;8to}yv9&54+hM(~zG01`PA0#)Vf6_v@?6%(K{BrG5TidxMy z<}z7XfEADm!2h!<->jSHKA_8C7845VMnG2l#6Y_Gjpq~Xjf(bhRqx%9GGGBzy?6k- zy6{-V;r8dLohDAYwapg&0_*`2p5!JpxBGus`uf<)-F3I9Dl0Q}iwG;*^5pw+l@H*j z6F(<-20VSnX!Lx$iU)qnbzdU20}Qx!eb*%aoZtnpg%^JR4``T;yPzJTvcd>_3=6G4 Uz+4pc{T-kfsl6y(7i=g60D~pRzW@LL literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..c4cd423b45115fb50739c1d949acec69018379a3 GIT binary patch literal 4502 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..47ecff70f48b10db2ba4c2b3113eb08e82031407 GIT binary patch literal 2986 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..db1a1d0ede4ab8458ada1e13a97424df90acc8ea GIT binary patch literal 6958 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..8559a4d2cf78557055af035796482126df0635e2 GIT binary patch literal 9534 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 + + #003767 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..d3c3afc --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +