feat(all): Init

This commit is contained in:
SamNofee 2024-01-19 00:32:38 +08:00
commit fb260289c7
70 changed files with 3487 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -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

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
Power

6
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

23
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State>
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_7_Android_10.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-09-12T06:59:11.653261Z" />
</State>
</entry>
</value>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.6.20" />
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

112
app/build.gradle Normal file
View File

@ -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"
}

137
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,137 @@
-keepattributes Exceptions,InnerClasses,*Annotation*,Signature,EnclosingMethod
-dontshrink
-dontoptimize
-dontpreverify
-dontnote
-ignorewarnings
-keepclassmembers enum * {
public static <methods>;
}
# 避免混淆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 <fields>;
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 <methods>;
}
#加固后的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 <methods>;
}
-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 {
<init>(...);
}
# ViewModel's empty constructor is considered to be unused by proguard
-keepclassmembers class * extends android.arch.lifecycle.ViewModel {
<init>(...);
}
# 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 {
<init>(...);
}
-keep class * implements android.arch.lifecycle.LifecycleObserver {
<init>(...);
}
-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.** {*;}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false"/>
<uses-feature
android:name="android.hardware.usb.accessory"
android:required="true"/>
<application
android:name="com.power.ops.MainApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
tools:replace="android:usesCleartextTraffic"
android:label="新能源智能"
android:hardwareAccelerated="true"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/Theme.Power"
tools:targetApi="33">
<meta-data
android:name="com.dji.sdk.API_KEY"
android:value="068356817738bbcc900bde2f"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Power">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
android:resource="@xml/accessory_filter" />
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,75 @@
package com.power.ops
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.power.ops.managers.DjiManager
import com.power.ops.managers.HttpManager
import com.power.ops.managers.LogManager
import com.power.ops.pages.AircraftPage
import com.power.ops.pages.AircraftSettingPage
import com.power.ops.pages.HomePage
import com.power.ops.pages.LoginPage
import com.power.ops.pages.MessagePage
import com.power.ops.pages.SettingPage
import com.power.ops.pages.SitePage
import com.power.ops.pages.SubmitPage
import com.power.ops.theme.PowerTheme
import com.power.ops.utils.redirectTo
import com.power.ops.vms.SettingVM
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PowerTheme {
Surface( modifier = Modifier.fillMaxSize() ) {
Nav()
}
}
}
DjiManager.registerApp(this)
}
}
@Composable
fun Nav() {
val navController = rememberNavController()
SettingVM.singleton.initDao(LocalContext.current)
// LaunchedEffect(navController) {
// val token = SettingVM.singleton.getSetting("token")
// if (token == null || token.value == "") {
// redirectTo(navController, "LoginPage")
// } else {
// HttpManager.singleton.token = token.value
// }
// }
NavHost(navController = navController, startDestination = "SitePage") {
composable("HomePage") { HomePage(navController) }
composable("AircraftPage") { AircraftPage(navController) }
composable("AircraftSettingPage") { AircraftSettingPage(navController) }
composable("MessagePage") { MessagePage(navController) }
composable("SitePage") { SitePage(navController) }
composable("SubmitPage") { SubmitPage(navController) }
composable("SettingPage") { SettingPage(navController) }
composable("LoginPage") { LoginPage(navController) }
}
}

View File

@ -0,0 +1,13 @@
package com.power.ops
import android.app.Application
import android.content.Context
import com.secneo.sdk.Helper
import com.power.ops.vms.SettingVM
class MainApplication : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
Helper.install(this)
}
}

View File

@ -0,0 +1,68 @@
package com.power.ops.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CrisisAlert
import androidx.compose.material.icons.rounded.FormatListBulleted
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.Message
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavController
import com.power.ops.theme.Main500
import com.power.ops.theme.PowerTheme
import com.power.ops.theme.Secondary400
import com.power.ops.theme.Typography
import com.power.ops.utils.redirectTo
data class BottomBarItem(
val icon: ImageVector,
val label: String,
val value: String
)
@Composable
fun BottomBar(navController: NavController, selected: String) {
val items = listOf<BottomBarItem>(
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")
}
}

View File

@ -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
)
}
}

View File

@ -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) ) {}
}

View File

@ -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<DropdownItem>) {
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)
}
}
}
}

View File

@ -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)
}
)
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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 = {})
}
}

View File

@ -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, "测试", {})
}
}

View File

@ -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}")
}
}

View File

@ -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<KMZPoint>)
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<MediaFile> = 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<Double> {
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<FileListItem> = 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<FileListItem> = 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<MediaFile>) {
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>(
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}") }
})
}
}
}

View File

@ -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()
}
}

View File

@ -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() }
}
}

View File

@ -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<String>) {
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() }
}
}

View File

@ -0,0 +1,17 @@
package com.power.ops.managers
import android.util.Log
class LogManager {
companion object {
val logs: ArrayList<String> = 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)
}
}
}

View File

@ -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<String, String>)
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<String, String>) {
val payload = Payload(map)
send(Gson().toJson(payload))
}
companion object {
val websocketClient: OkHttpClient by lazy { OkHttpClient() }
val singleton: WebsocketManager by lazy { WebsocketManager() }
}
}

View File

@ -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
// ) {
//
//
// }
}
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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")
}
}

View File

@ -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))
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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)

View File

@ -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
)
}

View File

@ -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
}

View File

@ -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<List<SettingEntity>>
@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<List<SettingEntity>>
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()
}
}

View File

@ -0,0 +1,63 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="213.37"
android:viewportHeight="213.84">
<group android:scaleX="0.3691868"
android:scaleY="0.37"
android:translateX="67.29831"
android:translateY="67.3596">
<path
android:pathData="M186.29,85.38c0.17,13.25 -1.41,26.98 -4.9,40.79 -3.79,14.99 -9.53,28.84 -16.77,41.17 -16.81,-5.74 -34.09,-14.98 -50.54,-27.44 3.66,-8.97 6.79,-18.47 9.3,-28.4 2.24,-8.84 3.87,-17.61 4.94,-26.23 2.58,-20.63 1.92,-40.35 -1.57,-57.88 -1.97,-9.91 -4.85,-19.11 -8.55,-27.39 3.75,0.54 7.51,1.28 11.25,2.23 15.6,3.94 29.55,11.11 41.28,20.64 2.25,4.13 4.28,8.46 6.07,12.95 5.97,14.99 9.29,31.84 9.49,49.56ZM84.25,189.61c-5.83,6.73 -12.01,12.62 -18.42,17.54 3.55,1.31 7.2,2.44 10.95,3.39 15.6,3.95 31.28,4.26 46.13,1.46 2.71,-1.76 5.38,-3.66 7.98,-5.68 -15.65,-2.5 -31.54,-8.1 -46.64,-16.71Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="153.27"
android:startY="53.83"
android:endX="114.4"
android:endY="166.97"
android:type="linear">
<item android:offset="0" android:color="#FF00D3D8"/>
<item android:offset="0.22" android:color="#FF00CBD5"/>
<item android:offset="0.55" android:color="#FF00B7CD"/>
<item android:offset="0.95" android:color="#FF0096C0"/>
<item android:offset="1" android:color="#FF0091BF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M128.32,85.27c-8.73,1.32 -17.62,3.23 -26.56,5.76 -9.59,2.72 -18.76,6.01 -27.42,9.79 -19,8.28 -35.54,18.88 -48.73,30.8 -6.9,6.23 -12.89,12.83 -17.82,19.64 -1.41,-3.51 -2.65,-7.13 -3.7,-10.85C-0.29,124.93 -1.05,109.27 1.33,94.34c2.12,-3.48 4.46,-6.88 6.99,-10.2 9.81,-12.83 22.62,-24.33 37.78,-33.57 11.79,-7.2 25.02,-13.04 39.36,-17.11 13.93,-3.94 27.83,-5.91 41.29,-6.07 3.49,17.53 4.15,37.25 1.57,57.88ZM210.83,81.87c-4.38,-15.49 -11.95,-29.22 -21.8,-40.68 -3.92,-2 -8.01,-3.79 -12.23,-5.37 5.97,14.99 9.29,31.84 9.49,49.56 9.75,1.69 18.85,4.27 27.08,7.67 -0.64,-3.73 -1.48,-7.46 -2.54,-11.18Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="10.66"
android:startY="103.3"
android:endX="186.47"
android:endY="52.56"
android:type="linear">
<item android:offset="0" android:color="#FF00D3D8"/>
<item android:offset="0.22" android:color="#FF00CBD5"/>
<item android:offset="0.54" android:color="#FF00B8CD"/>
<item android:offset="0.92" android:color="#FF0098C1"/>
<item android:offset="1" android:color="#FF0091BF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M192.97,173.76c-2.34,2.97 -4.86,5.86 -7.55,8.63 -11.22,11.54 -24.4,20.02 -38.52,25.42 -5.28,-0.13 -10.63,-0.63 -16.01,-1.49 -15.65,-2.5 -31.54,-8.1 -46.64,-16.71 -11.29,-6.4 -22.14,-14.49 -32.14,-24.21 -10.7,-10.4 -19.55,-21.83 -26.5,-33.78 13.19,-11.92 29.73,-22.52 48.73,-30.8 5.81,7.45 12.32,14.71 19.48,21.67 6.54,6.36 13.33,12.18 20.26,17.41 16.45,12.46 33.73,21.7 50.54,27.44 9.68,3.32 19.22,5.47 28.35,6.42ZM39.76,24.83c-2.91,2.42 -5.72,5.01 -8.41,7.79 -11.22,11.53 -19.33,24.95 -24.33,39.22 0.21,4.07 0.65,8.17 1.3,12.3 9.81,-12.83 22.62,-24.33 37.78,-33.57 -3.11,-8.76 -5.24,-17.4 -6.34,-25.74Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="144.67"
android:startY="191.51"
android:endX="13.5"
android:endY="61.56"
android:type="linear">
<item android:offset="0" android:color="#FF00D3D8"/>
<item android:offset="0.22" android:color="#FF00CBD5"/>
<item android:offset="0.55" android:color="#FF00B7CD"/>
<item android:offset="0.95" android:color="#FF0095C0"/>
<item android:offset="1" android:color="#FF0091BF"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@ -0,0 +1,58 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="213.37dp"
android:height="213.84dp"
android:viewportWidth="213.37"
android:viewportHeight="213.84">
<path
android:pathData="M186.29,85.38c0.17,13.25 -1.41,26.98 -4.9,40.79 -3.79,14.99 -9.53,28.84 -16.77,41.17 -16.81,-5.74 -34.09,-14.98 -50.54,-27.44 3.66,-8.97 6.79,-18.47 9.3,-28.4 2.24,-8.84 3.87,-17.61 4.94,-26.23 2.58,-20.63 1.92,-40.35 -1.57,-57.88 -1.97,-9.91 -4.85,-19.11 -8.55,-27.39 3.75,0.54 7.51,1.28 11.25,2.23 15.6,3.94 29.55,11.11 41.28,20.64 2.25,4.13 4.28,8.46 6.07,12.95 5.97,14.99 9.29,31.84 9.49,49.56ZM84.25,189.61c-5.83,6.73 -12.01,12.62 -18.42,17.54 3.55,1.31 7.2,2.44 10.95,3.39 15.6,3.95 31.28,4.26 46.13,1.46 2.71,-1.76 5.38,-3.66 7.98,-5.68 -15.65,-2.5 -31.54,-8.1 -46.64,-16.71Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="153.27"
android:startY="53.83"
android:endX="114.4"
android:endY="166.97"
android:type="linear">
<item android:offset="0" android:color="#FF00D3D8"/>
<item android:offset="0.22" android:color="#FF00CBD5"/>
<item android:offset="0.55" android:color="#FF00B7CD"/>
<item android:offset="0.95" android:color="#FF0096C0"/>
<item android:offset="1" android:color="#FF0091BF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M128.32,85.27c-8.73,1.32 -17.62,3.23 -26.56,5.76 -9.59,2.72 -18.76,6.01 -27.42,9.79 -19,8.28 -35.54,18.88 -48.73,30.8 -6.9,6.23 -12.89,12.83 -17.82,19.64 -1.41,-3.51 -2.65,-7.13 -3.7,-10.85C-0.29,124.93 -1.05,109.27 1.33,94.34c2.12,-3.48 4.46,-6.88 6.99,-10.2 9.81,-12.83 22.62,-24.33 37.78,-33.57 11.79,-7.2 25.02,-13.04 39.36,-17.11 13.93,-3.94 27.83,-5.91 41.29,-6.07 3.49,17.53 4.15,37.25 1.57,57.88ZM210.83,81.87c-4.38,-15.49 -11.95,-29.22 -21.8,-40.68 -3.92,-2 -8.01,-3.79 -12.23,-5.37 5.97,14.99 9.29,31.84 9.49,49.56 9.75,1.69 18.85,4.27 27.08,7.67 -0.64,-3.73 -1.48,-7.46 -2.54,-11.18Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="10.66"
android:startY="103.3"
android:endX="186.47"
android:endY="52.56"
android:type="linear">
<item android:offset="0" android:color="#FF00D3D8"/>
<item android:offset="0.22" android:color="#FF00CBD5"/>
<item android:offset="0.54" android:color="#FF00B8CD"/>
<item android:offset="0.92" android:color="#FF0098C1"/>
<item android:offset="1" android:color="#FF0091BF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M192.97,173.76c-2.34,2.97 -4.86,5.86 -7.55,8.63 -11.22,11.54 -24.4,20.02 -38.52,25.42 -5.28,-0.13 -10.63,-0.63 -16.01,-1.49 -15.65,-2.5 -31.54,-8.1 -46.64,-16.71 -11.29,-6.4 -22.14,-14.49 -32.14,-24.21 -10.7,-10.4 -19.55,-21.83 -26.5,-33.78 13.19,-11.92 29.73,-22.52 48.73,-30.8 5.81,7.45 12.32,14.71 19.48,21.67 6.54,6.36 13.33,12.18 20.26,17.41 16.45,12.46 33.73,21.7 50.54,27.44 9.68,3.32 19.22,5.47 28.35,6.42ZM39.76,24.83c-2.91,2.42 -5.72,5.01 -8.41,7.79 -11.22,11.53 -19.33,24.95 -24.33,39.22 0.21,4.07 0.65,8.17 1.3,12.3 9.81,-12.83 22.62,-24.33 37.78,-33.57 -3.11,-8.76 -5.24,-17.4 -6.34,-25.74Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="144.67"
android:startY="191.51"
android:endX="13.5"
android:endY="61.56"
android:type="linear">
<item android:offset="0" android:color="#FF00D3D8"/>
<item android:offset="0.22" android:color="#FF00CBD5"/>
<item android:offset="0.55" android:color="#FF00B7CD"/>
<item android:offset="0.95" android:color="#FF0095C0"/>
<item android:offset="1" android:color="#FF0091BF"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#003767</color>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Power" parent="android:Theme.Material.NoActionBar" />
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

26
build.gradle Normal file
View File

@ -0,0 +1,26 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
maven { url "https://jitpack.io" }
maven { url 'https://maven.fabric.io/public' }
maven { url 'https://plugins.gradle.org/m2/' }
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
maven { url "https://maven.mozilla.org/maven2/" }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath 'com.github.kezong:fat-aar:1.3.6'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1'
classpath "org.jacoco:org.jacoco.core:0.8.7"
}
}
plugins {
id 'com.android.application' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.6.20' apply false
}

4
gradle.properties Normal file
View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Mon Aug 07 18:20:51 CST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

22
settings.gradle Normal file
View File

@ -0,0 +1,22 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven { url "https://jitpack.io" }
maven { url "https://maven.mozilla.org/maven2/" }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
maven { url "https://maven.mozilla.org/maven2/" }
}
}
rootProject.name = "Power"
include ':app'