commit 154ad548bc1830a9655c8ba637d89509eb86c2f7 Author: shenzuqiang Date: Thu Feb 12 18:26:02 2026 +0800 Dev: init:项目初始化,具备完整UI和主体功能 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..265c37c --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +rabbit \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/MarsCodeWorkspaceAppSettings.xml b/.idea/MarsCodeWorkspaceAppSettings.xml new file mode 100644 index 0000000..da2e9da --- /dev/null +++ b/.idea/MarsCodeWorkspaceAppSettings.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/bidinfo.keystore b/app/bidinfo.keystore new file mode 100644 index 0000000..992fdaf Binary files /dev/null and b/app/bidinfo.keystore differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..946741c --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,155 @@ +plugins { + id("com.android.application") + alias(libs.plugins.compose.compiler) + kotlin("plugin.serialization") version "2.3.0" +} + +android { + namespace = "com.img.rabbit" + compileSdk = 36 + + buildFeatures { + compose = true + buildConfig = true + viewBinding = true + } + + + composeOptions { + kotlinCompilerExtensionVersion = "1.4.8" + } + + defaultConfig { + applicationId = "com.img.rabbit" + minSdk = 24 + targetSdk = 36 + versionCode = 2 + versionName = "1.1" + + + setManifestPlaceholders(mapOf( + "GETUI_APPID" to (project.findProperty("GETUI_APPID") as? String ?: ""), + "GT_INSTALL_CHANNEL" to (project.findProperty("GT_INSTALL_CHANNEL") as? String ?: "GT_INSTALL_CHANNEL") + )) + ndk { + abiFilters.addAll(listOf("arm64-v8a", "x86_64")) + } + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + vectorDrawables { + useSupportLibrary = true + } + multiDexEnabled = true + + resConfigs("en", "zh-rCN") + + flavorDimensions.addAll(listOf("channel")) + + productFlavors { + create("general") { + dimension = "channel" + manifestPlaceholders["UMENG_CHANNEL"] = "general" + } + } + + productFlavors.configureEach { + dimension = "channel" + manifestPlaceholders.putAll(mapOf("UMENG_CHANNEL" to name)) + } + manifestPlaceholders.putAll(mapOf( + "GETUI_APPID" to "40qbPjPkYs7TnVAYCX0Ig6", + "GT_INSTALL_CHANNEL" to "general", + )) + } + + + // 配置签名信息 + signingConfigs { + create("config") { + storeFile = file(project.findProperty("RELEASE_STORE_FILE") ?: "bidinfo.keystore") + storePassword = project.findProperty("RELEASE_STORE_PASSWORD") as? String ?: "" + keyAlias = project.findProperty("RELEASE_KEY_ALIAS") as? String ?: "" + keyPassword = project.findProperty("RELEASE_KEY_PASSWORD") as? String ?: "" + enableV1Signing = true + enableV2Signing = true + enableV3Signing = true + } + } + + buildTypes { + getByName("debug") { + isMinifyEnabled = false + isShrinkResources = false + signingConfig = signingConfigs.getByName("config") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + getByName("release") { + isMinifyEnabled = false + isShrinkResources = false + signingConfig = signingConfigs.getByName("config") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + // 基础依赖 + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core.splashscreen) + + // Compose 依赖 + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.activity.compose) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.navigation.runtime.ktx) + + // ViewModel 和 Lifecycle 依赖 + implementation(libs.androidx.compose.runtime.livedata) + implementation(libs.androidx.compose.lifecycle.viewmodel) + implementation(libs.androidx.compose.lifecycle.runtime.compose) + implementation(libs.androidx.compose.runtime.livedata) + + // Kotlinx Serialization 依赖 + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.datastore.core) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.foundation) + + // 测试依赖 + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.espresso.core) + + //个推一键认证 + implementation(libs.gysdk) + implementation(libs.gson) + implementation(libs.mmkv) + implementation(libs.coil.compose) + implementation(libs.coil.network.okhttp) + implementation(libs.segmentation.selfie) + implementation(libs.face.detection) + implementation(libs.android.gif.drawable) + implementation(libs.gif.encoder) + implementation("com.caverock:androidsvg:1.4") + implementation("io.github.lucksiege:pictureselector:v3.11.2") + // 压缩库 (可选,建议长图拼接前先压缩防止OOM) + implementation("io.github.lucksiege:compress:v3.11.2") + implementation("io.github.leavesczy:matisse:2.3.0") + implementation("com.github.moyuruaizawa:cropify:0.5.2") + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..b6e52b4 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,34 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# 友盟号码认证 +-keep class com.umeng.**{*;} + +-keepclassmembers class*{ + public (org.json.JSONObject); +} + +-keepclassmembers enum*{ + public static **[] values(); + public static ** valueOf(java.lang.String); +} +# 友盟号码认证 结束 diff --git a/app/release/app-release.apk b/app/release/app-release.apk new file mode 100644 index 0000000..7c353d8 Binary files /dev/null and b/app/release/app-release.apk differ diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm new file mode 100644 index 0000000..6ad59bf Binary files /dev/null and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm new file mode 100644 index 0000000..e64c21b Binary files /dev/null and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..981df34 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,37 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.img.rabbit", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 2, + "versionName": "1.1", + "outputFile": "app-release.apk" + } + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/app-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/app-release.dm" + ] + } + ], + "minSdkVersionForDexing": 24 +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/img/rabbit/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/img/rabbit/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..addc720 --- /dev/null +++ b/app/src/androidTest/java/com/img/rabbit/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.img.rabbit + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.img.rabbit", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1611682 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/BaseApplication.kt b/app/src/main/java/com/img/rabbit/BaseApplication.kt new file mode 100644 index 0000000..af420a6 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/BaseApplication.kt @@ -0,0 +1,32 @@ +package com.img.rabbit + +import android.app.Application +import android.util.Log +import com.img.rabbit.utils.NetworkMonitor +import com.g.gysdk.GYManager +import com.tencent.mmkv.MMKV + + +class BaseApplication : Application() { + private val TAG = "BaseApplication" + + override fun onCreate() { + super.onCreate() + // 初始化网络状态监控 + NetworkMonitor.initialize(this) + // 初始化MMKV + initMMKV() + // 初始化个推SDK + initGeTuiOneKeyLogin() + } + + private fun initGeTuiOneKeyLogin() { + GYManager.getInstance().preInit(this.applicationContext) + } + + private fun initMMKV() { + val rootDir = MMKV.initialize(this) + Log.i(TAG, "MMKV root dir: $rootDir") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/MainActivity.kt b/app/src/main/java/com/img/rabbit/MainActivity.kt new file mode 100644 index 0000000..7d3086e --- /dev/null +++ b/app/src/main/java/com/img/rabbit/MainActivity.kt @@ -0,0 +1,138 @@ +package com.img.rabbit + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.viewmodel.compose.viewModel +import com.img.rabbit.pages.LoginScreen +import com.img.rabbit.pages.MainScreen +import com.img.rabbit.viewmodel.GeneralViewModel +import com.img.rabbit.viewmodel.LoginViewModel +import com.img.rabbit.viewmodel.SplashViewModel +import kotlinx.coroutines.delay + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // 必须在 super.onCreate 之前调用 + val splashScreen = installSplashScreen() + super.onCreate(savedInstanceState) + + // 启用Edge-to-Edge模式(沉浸模式) + enableEdgeToEdge() + + setContent { + val splashViewModel: SplashViewModel = viewModel() + val generalViewModel: GeneralViewModel = viewModel() + val loginViewModel: LoginViewModel = viewModel() + + // 设置启动页显示条件 + splashScreen.setKeepOnScreenCondition { + splashViewModel.isLoading.value // 当为 true 时,启动页不消失 + } + + AppTheme { + SplashScreenContent { + val token = generalViewModel.kv.decodeString("token") + // 未登录,显示登录页 + if (token?.isNotEmpty() == false && !loginViewModel.isLogin.value) { + // 显示登录页 + LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) + } else { + //已登录,显示主页面 + MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) + } + } + } + + // 模拟加载过程,2秒后关闭启动页 + LaunchedEffect(Unit) { + delay(100L) + splashViewModel.setLoading(false) + } + } + } +} + + +@Composable +fun AppTheme(content: @Composable () -> Unit) { + // 使用Material3主题 + androidx.compose.material3.MaterialTheme { + content() + } +} + + +@Composable +fun SplashScreenContent( + onAnimationFinished: @Composable () -> Unit // 改为Composable函数类型 +) { + val scale = remember { Animatable(0f) } + var animationFinished by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + scale.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 800) + ) + animationFinished = true + } + + Box(modifier = Modifier.fillMaxSize()) { + if (!animationFinished) { + // 显示启动页动画 + Image( + painter = painterResource(id = R.mipmap.ic_splash_mask), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .scale(scale.value) + ) + Image( + painter = painterResource(id = R.mipmap.ic_splash_logo), + contentDescription = null, + modifier = Modifier + .align(Alignment.Center) + .wrapContentSize() + .scale(scale.value) + ) + } else { + // 动画完成后显示主界面 + onAnimationFinished() + } + } +} + + + +@Preview(showBackground = true) +@Composable +fun MainScreenPreview() { + AppTheme { + MainScreen(generalViewModel = viewModel(), loginViewModel = viewModel()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt b/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt new file mode 100644 index 0000000..bd26ebd --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt @@ -0,0 +1,11 @@ +package com.img.rabbit.bean + +data class ClothingBean( + //衣服索引(区分男女) + val index: Int, + //衣服id(不区分男女类别) + val id: Int, + val icon: Int, + val clothing: Int? =null, + val title: String +) diff --git a/app/src/main/java/com/img/rabbit/bean/FormatBean.kt b/app/src/main/java/com/img/rabbit/bean/FormatBean.kt new file mode 100644 index 0000000..8d5c80c --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/FormatBean.kt @@ -0,0 +1,7 @@ +package com.img.rabbit.bean + +data class FormatBean( + //格式id + val id: Int, + val title: String +) diff --git a/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt b/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt new file mode 100644 index 0000000..3079eeb --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt @@ -0,0 +1,11 @@ +package com.img.rabbit.bean + +data class HairstyleBean( + //发型索引(区分男女) + val index: Int, + //发型id(不区分男女类别) + val id: Int, + val icon: Int, + val hairstyle: Int? = null, + val title: String +) diff --git a/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt b/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt new file mode 100644 index 0000000..d83ab6a --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt @@ -0,0 +1,12 @@ +package com.img.rabbit.bean + +import android.graphics.Bitmap +import android.net.Uri + +data class LongImageBean( + val uri: Uri, + val bitmap: Bitmap, + var cropTop: Float = 0f, // 裁剪起始点(像素) + var displayHeight: Float = bitmap.height.toFloat() // 显示高度(像素) +) + diff --git a/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt b/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt new file mode 100644 index 0000000..4708b1a --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt @@ -0,0 +1,20 @@ +package com.img.rabbit.bean + +import kotlinx.serialization.Serializable + +/** + * 预登录数据模型 + * {"process_id":"982ec252c9bd82d06886fafc8a01b068","operatorType":"2","clienttype":"1","accessCode":"2ed00faebc5ceb5c55d5623e52672ae6e806d2ba3ef0a2018960165154037812","number":"186****0253","expiredTime":1770196189245,"errorCode":0,"errorDesc":"gysdk success!","costTime":3} + */ +@Serializable +data class OnekeyPreLogin( + val process_id: String, + val operatorType: String, + val clienttype: String, + val accessCode: String, + val number: String, + val expiredTime: Long, + val errorCode: Int, + val errorDesc: String, + val costTime: Int +) diff --git a/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt b/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt new file mode 100644 index 0000000..e8259ce --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt @@ -0,0 +1,10 @@ +package com.img.rabbit.bean + +data class ResizeBean( + //尺寸id + val id: Int, + val size: String, + val title: String, + val width: Float, + val height: Float +) diff --git a/app/src/main/java/com/img/rabbit/bean/UserInfo.kt b/app/src/main/java/com/img/rabbit/bean/UserInfo.kt new file mode 100644 index 0000000..fe4922f --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/UserInfo.kt @@ -0,0 +1,7 @@ +package com.img.rabbit.bean + +data class UserInfo( + val id: Int, + val name: String, + val login: Boolean, +) diff --git a/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt new file mode 100644 index 0000000..015c715 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt @@ -0,0 +1,1040 @@ +package com.img.rabbit.components + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableIntState +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.img.rabbit.R +import com.img.rabbit.bean.ClothingBean +import com.img.rabbit.bean.FormatBean +import com.img.rabbit.bean.HairstyleBean +import com.img.rabbit.bean.ResizeBean + +/** + * 底部画板(证件)选择器 + */ +@Composable +fun DrawingBoardCertificatePicker( + //尺寸 + sizes: MutableList, + selectedSize: MutableState +){ + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background(Color(0xFFFFFFFF), shape = RoundedCornerShape(18.dp)) + ) { + DrawingBoardSizePicker(sizes = sizes, selectedSize = selectedSize, singleSelect = false) + } +} + + +/** + * 底部画板(格式)选择器 + */ +@Composable +fun DrawingBoardFormatPicker( + formats: MutableList, + selectedFormat: MutableState +){ + val currentFormat = remember { mutableStateOf(selectedFormat.value ?: formats[0]) } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background(Color(0xFFFFFFFF), shape = RoundedCornerShape(18.dp)) + ) { + Box( + modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 6.dp) + ){ + Text( + text = "格式转换", + color = Color(0xFF000000), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center).padding(bottom = 6.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_cutout_base_color_tag), + contentDescription = "格式转换选择器", + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + + Box(modifier = Modifier.fillMaxWidth().height(12.dp)) + + Box(modifier = Modifier.fillMaxWidth().height(0.5.dp).background(Color(0xFFF4F4F4)).align(Alignment.CenterHorizontally)) + + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + .align(Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(12.dp), // 列表项之间的间距 + contentPadding = PaddingValues(horizontal = 16.dp) // 额外的左右内边距 + ) { + items(formats.size){ index -> + val item = formats[index] + FormatItem( + format = item, + isSelected = item.id == currentFormat.value.id, + onClick = { + currentFormat.value = item + selectedFormat.value = item + } + ) + } + } + + Box(modifier = Modifier.fillMaxWidth().height(20.dp)) + } +} + +/** + * 底部画板(外观)选择器 + */ +@Composable +fun DrawingBoardPicker( + //颜色 + colors: List, + //服装 + clothingForMans: MutableList, + clothingForFemales: MutableList, + //发型 + hairstyleForMans: MutableList, + hairstyleForFemales: MutableList, + //尺寸 + sizes: MutableList, + //选中 + selectedAppearance: MutableState,//底部:背景、服装、发型、尺寸 + selectedColorIndex: MutableIntState,//选中背景色索引 + selectedClothing: MutableState,//选中服装 + selectedHairstyle: MutableState,//选中发型 + selectedSize: MutableState,//选中尺寸 + onClothingSelected: (ClothingBean) -> Unit, + onHairstyleSelected: (HairstyleBean) -> Unit +){ + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background(Color(0xFFFFFFFF), shape = RoundedCornerShape(18.dp)) + ) { + + when (selectedAppearance.value) { + AppearanceType.BACKGROUND -> { + DrawingBoardBaseColorPicker(colors = colors,selectedColorIndex = selectedColorIndex) + } + AppearanceType.CLOTHING -> { + DrawingBoardClothingPicker(clothingForFemales = clothingForFemales, clothingForMans = clothingForMans, selectedClothing = selectedClothing, onClothingSelected = onClothingSelected) + } + AppearanceType.HAIRSTYLE -> { + DrawingBoardHairstylePicker(hairstyleForFemales = hairstyleForFemales, hairstyleForMans = hairstyleForMans, selectedHairstyle = selectedHairstyle, onHairstyleSelected = onHairstyleSelected) + } + AppearanceType.SIZE -> { + DrawingBoardSizePicker(sizes = sizes, selectedSize = selectedSize) + } + } + + Box(modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(top = 35.dp, start = 16.dp, end = 16.dp).background(Color(0xFFEDEDED))) + + Row( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 20.dp).align(Alignment.CenterHorizontally) + ) { + AppearancePicker( + selectedAppearance = selectedAppearance, + onAppearanceSelected = {appearance -> selectedAppearance.value = appearance } + ) + } + } +} + + +/** + * 底部画板(基础色) + * @param selectedColorIndex 选中的基础色索引 + */ +@Composable +fun DrawingBoardBaseColorPicker(colors: List,selectedColorIndex: MutableIntState){ + //底部画板(基础色) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp) + .wrapContentHeight() + ) { + Box( + modifier = Modifier.wrapContentWidth().wrapContentHeight().align(Alignment.CenterHorizontally) + ){ + Text( + text = "基础色", + color = Color(0xFF000000), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center).padding(bottom = 6.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_cutout_base_color_tag), + contentDescription = "颜色选择器", + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + + Row( + modifier = Modifier.fillMaxWidth().padding(top = 23.dp, start = 16.dp, end = 16.dp).align(Alignment.CenterHorizontally) + ) { + // 使用颜色选择控件 + ColorPicker( + colors = colors, + selectedIndex = selectedColorIndex.intValue, + onColorSelected = {index -> selectedColorIndex.intValue = index } + ) + } + } +} + + +/** + * 底部画板(衣服) + * @param clothingForFemales 女装衣服列表 + * @param clothingForMans 男装衣服列表 + * @param selectedClothing 选中的衣服 + */ +@Composable +fun DrawingBoardClothingPicker(clothingForFemales: MutableList, clothingForMans: MutableList, selectedClothing: MutableState, onClothingSelected: (ClothingBean) -> Unit){ + //注意:ClothingBean-id: 0~6 女装, 7~13 男装 + //女装(0) OR 男装(1) + val tab = remember { mutableIntStateOf(if(selectedClothing.value == null || selectedClothing.value!!.id<7) 0 else 1) } + //选中服装 + val currentClothing = remember { mutableStateOf(if(selectedClothing.value == null) clothingForFemales[0] else if(selectedClothing.value!!.id<7) clothingForFemales[selectedClothing.value!!.index] else clothingForMans[selectedClothing.value!!.index] ) } + + //底部画板(衣服) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp) + .background(Color(0xFFFFFFFF), shape = RoundedCornerShape(36.dp)) + ) { + + Row( + modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally), horizontalArrangement = Arrangement.SpaceEvenly + ) { + Column( + modifier = Modifier + .wrapContentSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { tab.intValue = 0 } + ) { + Box( + modifier = Modifier.wrapContentWidth().wrapContentHeight().align(Alignment.CenterHorizontally) + ){ + Text( + text = "女装", + color = if(tab.intValue == 0) Color(0xFF1A1A1A) else Color(0xFFAAAAAA), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center).padding(bottom = 6.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_cutout_base_color_tag), + contentDescription = "女装选择器", + modifier = Modifier.alpha(if(tab.intValue == 0) 1f else 0f).align(Alignment.BottomCenter) + ) + } + } + + Column( + modifier = Modifier + .wrapContentSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { tab.intValue = 1 } + ) { + Box( + modifier = Modifier.wrapContentWidth().wrapContentHeight().align(Alignment.CenterHorizontally) + ){ + Text( + text = "男装", + color = if(tab.intValue == 1) Color(0xFF1A1A1A) else Color(0xFFAAAAAA), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center).padding(bottom = 6.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_cutout_base_color_tag), + contentDescription = "男装选择器", + modifier = Modifier.alpha(if(tab.intValue == 1) 1f else 0f).align(Alignment.BottomCenter) + ) + } + } + } + + + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(12.dp), // 列表项之间的间距 + contentPadding = PaddingValues(horizontal = 16.dp) // 额外的左右内边距 + ) { + if (tab.intValue == 0){ + items(clothingForFemales.size){ index -> + val item = clothingForFemales[index] + ClothingItem( + clothing = item, + isSelected = item.id == currentClothing.value.id, + onClick = { + currentClothing.value = item + selectedClothing.value = item + onClothingSelected(item) + } + ) + } + }else{ + items(clothingForMans.size){ index -> + val item = clothingForMans[index] + ClothingItem( + clothing = item, + isSelected = item.id == currentClothing.value.id, + onClick = { + currentClothing.value = item + selectedClothing.value = item + onClothingSelected(item) + } + ) + } + } + } + + + } +} + + +/** + * 底部画板(发型) + * @param hairstyleForFemales 女装发型列表 + * @param hairstyleForMans 男装发型列表 + * @param selectedHairstyle 选中的发型 + */ +@Composable +fun DrawingBoardHairstylePicker(hairstyleForFemales: MutableList, hairstyleForMans: MutableList, selectedHairstyle: MutableState, onHairstyleSelected: (HairstyleBean) -> Unit){ + //女生(0) OR 男生(1)发型 + val tab = remember { mutableIntStateOf(if(selectedHairstyle.value == null || selectedHairstyle.value!!.id<7) 0 else 1) } + //选中发型 + val currentHairstyle = remember { mutableStateOf(if(selectedHairstyle.value == null) hairstyleForFemales[0] else if(selectedHairstyle.value!!.id<7) hairstyleForFemales[selectedHairstyle.value!!.index] else hairstyleForMans[selectedHairstyle.value!!.index] ) } + + //底部画板(发型) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp) + .background(Color(0xFFFFFFFF), shape = RoundedCornerShape(36.dp)) + ) { + + Row( + modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally), horizontalArrangement = Arrangement.SpaceEvenly + ) { + Column( + modifier = Modifier + .wrapContentSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { tab.intValue = 0 } + ) { + Box( + modifier = Modifier.wrapContentWidth().wrapContentHeight() + ){ + Text( + text = "女生", + color = if(tab.intValue == 0) Color(0xFF1A1A1A) else Color(0xFFAAAAAA), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center).padding(bottom = 6.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_cutout_base_color_tag), + contentDescription = "女生发型选择器", + modifier = Modifier.alpha(if(tab.intValue == 0) 1f else 0f).align(Alignment.BottomCenter) + ) + } + } + + + Column( + modifier = Modifier + .wrapContentSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { tab.intValue = 1 } + ) { + Box( + modifier = Modifier.wrapContentWidth().wrapContentHeight() + ){ + Text( + text = "男生", + color = if(tab.intValue == 1) Color(0xFF1A1A1A) else Color(0xFFAAAAAA), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center).padding(bottom = 6.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_cutout_base_color_tag), + contentDescription = "男生发型选择器", + modifier = Modifier.alpha(if(tab.intValue == 1) 1f else 0f).align(Alignment.BottomCenter) + ) + } + } + } + + + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(12.dp), // 列表项之间的间距 + contentPadding = PaddingValues(horizontal = 16.dp) // 额外的左右内边距 + ) { + if (tab.intValue == 0){ + items(hairstyleForFemales.size){ index -> + val item = hairstyleForFemales[index] + HairstyleItem( + hairstyle = item, + isSelected = item.id == currentHairstyle.value.id, + onClick = { + currentHairstyle.value = item + selectedHairstyle.value = item + onHairstyleSelected(item) + } + ) + } + }else{ + items(hairstyleForMans.size){ index -> + val item = hairstyleForMans[index] + HairstyleItem( + hairstyle = item, + isSelected = item.id == currentHairstyle.value.id, + onClick = { + currentHairstyle.value = item + selectedHairstyle.value = item + onHairstyleSelected(item) + } + ) + } + } + } + + + } +} + +@Composable +fun DrawingBoardSizePicker(sizes: MutableList, selectedSize: MutableState, singleSelect: Boolean = true) { + //选中尺寸 + val currentSize = remember { mutableStateOf(selectedSize.value) } + //底部画板(尺寸) + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 6.dp,bottom = if(!singleSelect) 20.dp else 0.dp) + ) { + Box( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ){ + Text( + text = "选择尺寸", + color = Color(0xFF000000), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center).padding(bottom = 6.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_cutout_base_color_tag), + contentDescription = "尺寸选择器", + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + + if(!singleSelect){ + Box(modifier = Modifier.fillMaxWidth().height(12.dp)) + + Box(modifier = Modifier.fillMaxWidth().height(0.5.dp).background(Color(0xFFF4F4F4)).align(Alignment.CenterHorizontally)) + } + + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + .align(Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(12.dp), // 列表项之间的间距 + contentPadding = PaddingValues(horizontal = 16.dp) // 额外的左右内边距 + ) { + items(sizes.size){ index -> + val item = sizes[index] + SizeItem( + item = item, + isSelected = item.id == currentSize.value.id, + onClick = { + currentSize.value = item + selectedSize.value = item + } + ) + } + } + + + if(singleSelect){ + + Box( modifier = Modifier.fillMaxWidth().height(12.dp) ) + Box( modifier = Modifier.fillMaxWidth().height(0.5.dp).background(Color(0xFFF4F4F4)).align(Alignment.CenterHorizontally) ) + } + } +} + + + +/** + * 底部画板(外观) + * @param selectedAppearance 选中的外观类型 + * @param onAppearanceSelected 外观选择回调 + */ +@Composable +fun AppearancePicker( + selectedAppearance: MutableState, + onAppearanceSelected: (AppearanceType) -> Unit, + modifier: Modifier = Modifier +) { + // 定义颜色列表(与您提供的示例完全匹配) + val appearances = remember { + listOf( + AppearanceType.BACKGROUND, // 第一个:背景 + AppearanceType.CLOTHING, // 第二个:服装 + AppearanceType.HAIRSTYLE, // 第三个:发型 + AppearanceType.SIZE // 最后一个:尺寸 + ) + } + + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + appearances.forEachIndexed { _, appearance -> + AppearanceOption( + appearance = appearance, + isSelected = appearance == selectedAppearance.value, + onClick = { onAppearanceSelected(appearance) }, + ) + } + } +} + +/** + * 底部画板(外观)选项 + * @param appearance 外观类型 + * @param isSelected 是否选中 + * @param onClick 点击回调 + */ +@Composable +private fun AppearanceOption( + appearance: AppearanceType, + isSelected: Boolean, + onClick: (AppearanceType) -> Unit, +){ + Box( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .background( + Color(0x00000000), + shape = RoundedCornerShape(359.dp), + ) + .border( + width = 1.dp, + color = if(isSelected) Color(0xFF000000) else Color(0xFFEDEDED), + shape = RoundedCornerShape(359.dp) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick(appearance) + } + ) { + Row( + modifier = Modifier + .align(Alignment.Center) + .padding(start = 11.dp, end = 18.dp) + .alpha(if(isSelected) 1f else 0f) + ) { + Image( + painter = painterResource(id = appearance.icon), + contentDescription = "图标", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterVertically) + //.alpha(if(isSelected) 1f else 0f) + ) + Text( + appearance.title, + color = if(isSelected) Color(0xFF1A1A1A) else Color(0xFFAAAAAA), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 7.dp) + ) + } + Text( + appearance.title, + color = if(isSelected) Color(0xFF1A1A1A) else Color(0xFFAAAAAA), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.Center) + .padding(vertical = 7.dp) + .alpha(if(!isSelected) 1f else 0f), + textAlign = TextAlign.Center + ) + } +} + +/** + * 底部画板(衣服)选项 + * @param clothing 衣服类型 + * @param isSelected 是否选中 + * @param onClick 点击回调 + */ +@Composable +private fun ClothingItem( + clothing: ClothingBean, + isSelected: Boolean, + onClick: (ClothingBean) -> Unit, +){ + + Box( + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + onClick(clothing) + } + .width(65.dp) + .height(82.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFFF4F4F4), + Color(0x00FFFFFF) + ) + ), + shape = RoundedCornerShape(4.dp) + ).border( + width = 1.86.dp, + color = if(isSelected) Color(0xFFC2FF43) else Color(0x00000000), + shape = RoundedCornerShape(4.dp) + ) + ){ + Image( + painter = painterResource(id = clothing.icon), + contentDescription = "图标", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.Center) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(16.dp) + .align(Alignment.BottomCenter) + .background( + Color(0x4D000000), + shape = RoundedCornerShape(4.dp) + ) + ){ + Text( + clothing.title, + color = Color(0xFF1A1A1A), + fontSize = 10.sp, + lineHeight = 10.sp, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.Center) + ) + } + } + +} + +/** + * 底部画板(发型)选项 + * @param hairstyle 发型类型 + * @param isSelected 是否选中 + * @param onClick 点击回调 + */ +@Composable +private fun HairstyleItem( + hairstyle: HairstyleBean, + isSelected: Boolean, + onClick: (HairstyleBean) -> Unit, +){ + + Box( + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + onClick(hairstyle) + } + .width(65.dp) + .height(82.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFFF4F4F4), + Color(0x00FFFFFF) + ) + ), + shape = RoundedCornerShape(4.dp) + ).border( + width = 1.86.dp, + color = if(isSelected) Color(0xFFC2FF43) else Color(0x00000000), + shape = RoundedCornerShape(4.dp) + ) + ){ + Image( + painter = painterResource(id = hairstyle.icon), + contentDescription = "图标", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.Center) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(16.dp) + .align(Alignment.BottomCenter) + .background( + Color(0x4D000000), + shape = RoundedCornerShape(4.dp) + ) + ){ + Text( + hairstyle.title, + color = Color(0xFF1A1A1A), + fontSize = 10.sp, + lineHeight = 10.sp, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.Center) + ) + } + } +} + +@Composable +fun SizeItem( + item: ResizeBean, + isSelected: Boolean, + onClick: (ResizeBean) -> Unit, +){ + Box( + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + onClick(item) + } + .width(70.dp) + .height(60.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFFF4F4F4), + Color(0x00FFFFFF) + ) + ), + shape = RoundedCornerShape(4.dp) + ) + .border( + width = if(isSelected) 1.86.dp else 0.5.dp, + color = if(isSelected) Color(0xFFC2FF43) else Color(0xFFEDEDED), + shape = RoundedCornerShape(4.dp) + ) + ) { + Column( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.Center) + ) { + Text( + item.title, + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + lineHeight = 12.sp, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + ) + Text( + item.size, + color = Color(0xFFAAAAAA), + fontSize = 10.sp, + lineHeight = 10.sp, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + ) + } + } +} + + + +@Composable +fun FormatItem( + format: FormatBean, + isSelected: Boolean, + onClick: (FormatBean) -> Unit, +){ + Box( + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick(format) + } + .wrapContentSize() + .background(color = Color(0xFFFFFFFF), shape = RoundedCornerShape(36.dp)).border( + width = 1.dp, + color = if(isSelected) Color(0xFF000000) else Color(0xFFEDEDED), + shape = RoundedCornerShape(36.dp) + ) + ) { + Column( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(horizontal = 21.dp, vertical = 7.dp) + .align(Alignment.Center) + ) { + Text( + format.title, + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + lineHeight = 12.sp, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + ) + } + } +} + +/** + * 颜色选择控件 + * + * @param selectedIndex 当前选中的颜色索引 + * @param onColorSelected 颜色选中回调 + * @param modifier 修饰符 + */ +@Composable +fun ColorPicker( + colors: List, + selectedIndex: Int, + onColorSelected: (Int) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + colors.forEachIndexed { index, color -> + ColorOption( + size = colors.size, + index = index, + color = color, + isSelected = index == selectedIndex, + onClick = { onColorSelected(index) }, + isBordered = index == 0 || index == colors.lastIndex // 第一个和最后一个带边框 + ) + } + } +} + +/** + * 颜色选项组件 + * + * @param color 颜色 + * @param isSelected 是否选中 + * @param onClick 点击事件 + * @param isBordered 是否显示边框 + */ +@Composable +private fun ColorOption( + size: Int, + index: Int, + color: Color, + isSelected: Boolean, + onClick: () -> Unit, + isBordered: Boolean = false +) { + val interactionSource = remember { MutableInteractionSource() } + val baseSize = 22.dp + val selectedSize = 24.dp + + // 构建修饰符 + var modifier = Modifier + .size(if (isSelected) selectedSize else baseSize) + .clip(CircleShape) + .background(color) + + // 边框逻辑 + val borderColor = + /* + if (isSelected) { + Color.Red // 选中时的红色边框 + } else + */ + if (isBordered) { + Color(0xFFAAAAAA) // 普通边框颜色 + } else { + null + } + + borderColor?.let { + modifier = modifier.border( + width = if (isSelected) 0.5.dp else 0.5.dp, + color = it, + shape = CircleShape + ) + } + + if(index == 0){ + Box(modifier = Modifier.wrapContentSize()){ + Image( + painter = painterResource(id = R.mipmap.ic_cutout_color_less), + contentScale = ContentScale.FillWidth, + contentDescription = "颜色选择器", + modifier = Modifier + .size(if (isSelected) selectedSize else baseSize), + alignment = Alignment.BottomCenter + ) + Box( + modifier = modifier.clickable( + indication = null, + interactionSource = interactionSource + ) { onClick() }, + contentAlignment = Alignment.Center + ) { + // 可以根据需要添加额外内容 + } + + if(isSelected){ + Box( + modifier = Modifier.size(18.dp).clip(CircleShape).background(Color.Transparent).border( + width = 2.dp, + color = Color(0xFFAAAAAA), + shape = CircleShape + ).align(Alignment.Center) + ) + } + } + }else{ + + Box(modifier = Modifier.wrapContentSize()){ + Box( + modifier = modifier.clickable( + indication = null, + interactionSource = interactionSource + ) { onClick() }, + contentAlignment = Alignment.Center + ) { + // 可以根据需要添加额外内容 + } + + if(isSelected){ + Box( + modifier = Modifier.size(20.dp).clip(CircleShape).background(Color.Transparent).border( + width = 2.dp, + color = if(index == size-1) Color(0xFFAAAAAA) else Color(0xFFFFFFFF), + shape = CircleShape + ).align(Alignment.Center) + ) + } + } + } +} + +/** + * 底部画板(外观)类型 + */ +enum class AppearanceType(val type: String, val title: String, val icon: Int) { + BACKGROUND(type = "background", title = "背景", icon = R.mipmap.ic_cutout_bg), + CLOTHING(type = "clothing", title = "服装", icon = R.mipmap.ic_cutout_clothing), + HAIRSTYLE(type = "hairstyle", title = "发型", icon = R.mipmap.ic_cutout_hairstyle), + SIZE(type = "size", title = "尺寸", icon = R.mipmap.ic_cutout_size) +} + +@SuppressLint("UnrememberedMutableState") +@Preview(showBackground = true) +@Composable +fun PreviewDrawingBoardHairstylePicker(){ + DrawingBoardHairstylePicker(hairstyleForMans = mutableListOf(), hairstyleForFemales = mutableListOf(), selectedHairstyle = mutableStateOf(null), onHairstyleSelected = {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/components/ImagePicker.kt b/app/src/main/java/com/img/rabbit/components/ImagePicker.kt new file mode 100644 index 0000000..80b14af --- /dev/null +++ b/app/src/main/java/com/img/rabbit/components/ImagePicker.kt @@ -0,0 +1,443 @@ +package com.img.rabbit.components + +import android.Manifest +import android.annotation.SuppressLint +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.core.content.FileProvider +import java.io.File + +// 首先添加ImageUtils的导入 +import android.graphics.Bitmap.CompressFormat +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.ui.res.painterResource +import coil3.compose.AsyncImage +import com.img.rabbit.R +import com.img.rabbit.utils.ImageUtils +import kotlin.apply +import kotlin.collections.all +import kotlin.collections.minus +import kotlin.collections.plus +import kotlin.collections.toMutableList +import kotlin.let + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImagePicker( + modifier: Modifier = Modifier, + imageHeight: Dp = 100.dp, + aspectRatio: Float = 100f / 100f, + maxCount: Int = 3, + addButtonName: String = "添加图片", + currentImageUris: List = emptyList(), + currentImagePaths: List = emptyList(), // 新增参数 + onImagesUpdated: (uris: List, paths: List) -> Unit +) { + val context = LocalContext.current + // 添加临时文件管理 + val tempImageUri = remember { mutableStateOf(null) } + + // 记录当前操作场景 + var currentScene by remember { mutableStateOf(null) } + // 新增选择对话框 + var showChoiceDialog by remember { mutableStateOf(false) } + + // 新增大图预览状态 + var previewImageUri by remember { mutableStateOf(null) } + + // 图片路径转Uri的辅助函数 + fun getDisplayImages(): List { + return currentImageUris.ifEmpty { + currentImagePaths.mapNotNull { path -> + try { + Uri.fromFile(File(path)) + } catch (e: Exception) { + null + } + } + } + } + + /* + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + result.data?.data?.let { uri -> + if (currentImages.size < maxCount) { + onImagesUpdated(currentImages + uri) + } + } + } + */ + + // 相册选择逻辑 + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri -> + uri?.let { + if (currentImageUris.size < maxCount) { + // 保存图片到存储并获取路径 + val savedPath = ImageUtils.saveUriToStorage( + context = context, + uri = it, + format = CompressFormat.JPEG, + quality = 90 + ) + // 更新图片列表和路径列表 + onImagesUpdated( + currentImageUris + it, + if (savedPath != null) currentImagePaths + savedPath else currentImagePaths + ) + } + } + } + + // 拍照逻辑 + val cameraLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.TakePicture() + ) { success -> + if (success) { + tempImageUri.value?.let { uri -> + if (currentImageUris.size < maxCount) { + // 保存图片到存储并获取路径 + val savedPath = ImageUtils.saveUriToStorage( + context = context, + uri = uri, + format = CompressFormat.JPEG, + quality = 90 + ) + // 更新图片列表和路径列表 + onImagesUpdated( + currentImageUris + uri, + if (savedPath != null) currentImagePaths + savedPath else currentImagePaths + ) + } + } + } + } + + // 权限检查 + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + // 修改权限回调逻辑 + if (permissions.all { it.value }) { + when (currentScene) { + PickerScene.GALLERY -> galleryLauncher.launch("image/*") + PickerScene.CAMERA -> { + val uri = FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + File.createTempFile("IMG_", ".jpg", context.externalCacheDir) + ) + tempImageUri.value = uri + cameraLauncher.launch(uri) + } + null -> { /* 无操作 */ } + } + } + } + + + if (showChoiceDialog) { + AlertDialog( + onDismissRequest = { showChoiceDialog = false }, + title = { Text(text = "选择图片来源", fontSize = 14.sp) }, + text = { Text(text = "请选择拍照或从相册选取, 若无权限点击相册或者拍照后会弹出权限请求", fontSize = 12.sp) }, + confirmButton = { + TextButton( + onClick = { + showChoiceDialog = false + + currentScene = PickerScene.CAMERA + permissionLauncher.launch( + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + ) + } + ) { Text(text = "拍照", fontSize = 14.sp) } + }, + dismissButton = { + TextButton( + onClick = { + showChoiceDialog = false + + currentScene = PickerScene.GALLERY + permissionLauncher.launch( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) + } + ) { Text(text = "相册", fontSize = 14.sp) } + } + ) + } + + // 全屏大图对话框 + previewImageUri?.let { uri -> + FullScreenDialog( + onDismiss = { previewImageUri = null } + ) { + ZoomableImage( + uri = uri, + modifier = Modifier + .fillMaxSize() + ) + } + } + + LazyRow(modifier = modifier.defaultMinSize(minHeight = (imageHeight + 8.dp))) { + items(getDisplayImages()) { uri -> + Box( + Modifier.padding(end = 6.dp, top = 6.dp).background(color = Color(0x80E5E5E7), shape = RoundedCornerShape(8.dp)) + ) { + AsyncImage( + model = uri, + contentDescription = null, + modifier = Modifier + .height(imageHeight) + .aspectRatio(aspectRatio) + .clip(RoundedCornerShape(8.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + previewImageUri = uri + } + ) + val size = if(imageHeight < 150.dp){ + 24.dp + }else{ + 32.dp + } + Image( + painter = painterResource(id = R.mipmap.ic_picture_del), + contentDescription = null, + modifier = Modifier + .size(size) + .aspectRatio(1f) // 设置宽高比为1:1 + .align(Alignment.TopEnd) + .padding(top = 4.dp, end = 4.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + // 找到当前uri在列表中的索引,同时从paths列表中移除对应的路径 + val uriIndex = currentImageUris.indexOf(uri) + val newPaths = if (uriIndex >= 0 && uriIndex < currentImagePaths.size) { + currentImagePaths.toMutableList().apply { removeAt(uriIndex) } + } else { + // 如果uri不在currentImageUris中,可能是从currentImagePaths转换过来的 + // 尝试通过路径匹配找到对应的索引 + val pathIndex = currentImagePaths.indexOf(uri.path) + if (pathIndex >= 0) { + currentImagePaths.toMutableList().apply { removeAt(pathIndex) } + } else { + currentImagePaths + } + } + onImagesUpdated(currentImageUris - uri, newPaths) + } + ) + /* + IconButton( + onClick = { + // 找到当前uri在列表中的索引,同时从paths列表中移除对应的路径 + val uriIndex = currentImageUris.indexOf(uri) + val newPaths = if (uriIndex >= 0 && uriIndex < currentImagePaths.size) { + currentImagePaths.toMutableList().apply { removeAt(uriIndex) } + } else { + // 如果uri不在currentImageUris中,可能是从currentImagePaths转换过来的 + // 尝试通过路径匹配找到对应的索引 + val pathIndex = currentImagePaths.indexOf(uri.path) + if (pathIndex >= 0) { + currentImagePaths.toMutableList().apply { removeAt(pathIndex) } + } else { + currentImagePaths + } + } + onImagesUpdated(currentImageUris - uri, newPaths) + }, + modifier = Modifier.align(Alignment.TopEnd) + ) { + Icon(Icons.Default.Close, "删除") + } + */ + } + } + + item { + if (currentImageUris.size < maxCount && currentImagePaths.size < maxCount) { + // 在触发按钮中添加 + if(maxCount==1){ + AddSingleButton( + imageHeight = imageHeight, + aspectRatio = aspectRatio, + onClick = { showChoiceDialog = true }, + label = addButtonName, + ) + }else{ + Box( + Modifier.defaultMinSize(minHeight = (imageHeight + 8.dp)), + contentAlignment = Alignment.BottomCenter + ) { + AddButton( + onClick = { showChoiceDialog = true }, + label = addButtonName, + ) + } + } + } + } + } +} + +@Composable +private fun AddButton( + onClick: () -> Unit, + label: String = "添加图片", + imageHeight: Dp = 100.dp, + aspectRatio: Float = 100f / 100f +) { + TextButton( + onClick = onClick, + modifier = Modifier + .height(imageHeight) + .aspectRatio(aspectRatio) + .border(1.dp, Color(0xFFA5A5A5), RoundedCornerShape(8.dp)) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon(Icons.Default.Add, contentDescription = label) + Text(text = label) + } + } +} + +@Composable +private fun AddSingleButton( + imageHeight: Dp = 100.dp, + aspectRatio: Float = 100f / 100f, + onClick: () -> Unit, + label: String = "添加图片" +) { + TextButton( + onClick = onClick, + modifier = Modifier + .height(imageHeight + 8.dp) + .aspectRatio(aspectRatio) + .border(1.dp, Color(0xFFA5A5A5), RoundedCornerShape(8.dp)) + ) { + if(label == "上传头像"){ + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { + Text(text = label) + } + }else{ + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { + Icon(Icons.Default.Add, contentDescription = label) + Text(text = label) + } + } + } +} + + +@Composable +private fun FullScreenDialog( + onDismiss: () -> Unit, + content: @Composable () -> Unit +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + usePlatformDefaultWidth = false, + decorFitsSystemWindows = false + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.95f)) + .clickable(onClick = onDismiss) + ) { + content() + } + } +} + +@SuppressLint("UnusedBoxWithConstraintsScope", "AutoboxingStateValueProperty") +@Composable +private fun ZoomableImage(uri: Uri, modifier: Modifier = Modifier) { + val scale = remember { mutableFloatStateOf(1f) } + val offset = remember { mutableStateOf(Offset.Zero) } + + BoxWithConstraints(modifier = modifier.pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> + scale.floatValue *= zoom + offset.value += pan + } + }) { + AsyncImage( + model = uri, + contentDescription = null, + modifier = Modifier + .graphicsLayer { + scaleX = scale.value + scaleY = scale.value + translationX = offset.value.x + translationY = offset.value.y + } + .align(Alignment.Center) + ) + } +} + +// 添加场景枚举 +enum class PickerScene { GALLERY, CAMERA } \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/config/Common.kt b/app/src/main/java/com/img/rabbit/config/Common.kt new file mode 100644 index 0000000..3dc9b14 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/config/Common.kt @@ -0,0 +1,6 @@ +package com.img.rabbit.config + +object Common { + const val privacyUrl = "https://www.baidu.com" + const val agreementUrl = "https://www.baidu.com" +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/config/CommonData.kt b/app/src/main/java/com/img/rabbit/config/CommonData.kt new file mode 100644 index 0000000..98a974c --- /dev/null +++ b/app/src/main/java/com/img/rabbit/config/CommonData.kt @@ -0,0 +1,91 @@ +package com.img.rabbit.config + +import androidx.compose.ui.graphics.Color +import com.img.rabbit.R +import com.img.rabbit.bean.ClothingBean +import com.img.rabbit.bean.FormatBean +import com.img.rabbit.bean.HairstyleBean +import com.img.rabbit.bean.ResizeBean + +object CommonData { + //背景颜色 + val colors = listOf( + Color.Transparent, // 第一个:带边框的透明 + Color.Red, // 第二个:红色 + Color(0xFFC62828), // 第三个:深红色 + Color(0xFFB71C1C), // 第四个:暗红色 + Color(0xFF2196F3), // 第五个:蓝色 + Color(0xFF1565C0), // 第六个:深蓝色 + Color(0xFF0277BD), // 第七个:亮蓝色 + Color.Transparent // 最后一个:带边框的透明 + ) + // 女装 + val clothingForFemales = mutableListOf().apply { + add(ClothingBean(index = 0, id = 0, icon = R.mipmap.ic_cutout_color_less, clothing = null,title = "无")) + add(ClothingBean(index = 1, id = 1, icon = R.mipmap.ic_clothing_female_1, clothing = R.mipmap.ic_clothing_female_1, title = "青春")) + add(ClothingBean(index = 2, id = 2, icon = R.mipmap.ic_clothing_female_2, clothing = R.mipmap.ic_clothing_female_2, title = "学生")) + add(ClothingBean(index = 3, id = 3, icon = R.mipmap.ic_clothing_female_3, clothing = R.mipmap.ic_clothing_female_3, title = "职场")) + add(ClothingBean(index = 4, id = 4, icon = R.mipmap.ic_clothing_female_4, clothing = R.mipmap.ic_clothing_female_4, title = "休闲")) + add(ClothingBean(index = 5, id = 5, icon = R.mipmap.ic_clothing_female_5, clothing = R.mipmap.ic_clothing_female_5, title = "日韩")) + add(ClothingBean(index = 6, id = 6, icon = R.mipmap.ic_clothing_female_6, clothing = R.mipmap.ic_clothing_female_6, title = "艺术")) + } + + // 男装 + val clothingForMans = mutableListOf().apply { + add(ClothingBean(index = 0, id = 7, icon = R.mipmap.ic_cutout_color_less, clothing = null, title = "无")) + add(ClothingBean(index = 1, id = 8, icon = R.mipmap.ic_clothing_man_2, clothing = R.mipmap.ic_clothing_man_2, title = "青春")) + add(ClothingBean(index = 2, id = 9, icon = R.mipmap.ic_clothing_man_3, clothing = R.mipmap.ic_clothing_man_3, title = "学生")) + add(ClothingBean(index = 3, id = 10, icon = R.mipmap.ic_clothing_man_4, clothing = R.mipmap.ic_clothing_man_4, title = "职场")) + add(ClothingBean(index = 4, id = 11, icon = R.mipmap.ic_clothing_man_5, clothing = R.mipmap.ic_clothing_man_5, title = "休闲")) + add(ClothingBean(index = 5, id = 12, icon = R.mipmap.ic_clothing_man_6, clothing = R.mipmap.ic_clothing_man_6, title = "日韩")) + add(ClothingBean(index = 6, id = 13, icon = R.mipmap.ic_clothing_man_7, clothing = R.mipmap.ic_clothing_man_7, title = "艺术")) + } + + + // 定义发型列表-女生 + val hairstyleForFemales = mutableListOf().apply { + add(HairstyleBean(index = 0, id = 0, icon = R.mipmap.ic_cutout_color_less, hairstyle = null, title = "无")) + add(HairstyleBean(index = 1, id = 1, icon = R.mipmap.ic_hairstyle_female_1, hairstyle = R.mipmap.ic_hairstyle_female_1, title = "青春")) + add(HairstyleBean(index = 2, id = 2, icon = R.mipmap.ic_hairstyle_female_2, hairstyle = R.mipmap.ic_hairstyle_female_2, title = "学生")) + add(HairstyleBean(index = 3, id = 3, icon = R.mipmap.ic_hairstyle_female_3, hairstyle = R.mipmap.ic_hairstyle_female_3, title = "职场")) + add(HairstyleBean(index = 4, id = 4, icon = R.mipmap.ic_hairstyle_female_4, hairstyle = R.mipmap.ic_hairstyle_female_4, title = "休闲")) + add(HairstyleBean(index = 5, id = 5, icon = R.mipmap.ic_hairstyle_female_5, hairstyle = R.mipmap.ic_hairstyle_female_5, title = "日韩")) + add(HairstyleBean(index = 6, id = 6, icon = R.mipmap.ic_hairstyle_female_6, hairstyle = R.mipmap.ic_hairstyle_female_6, title = "艺术")) + } + + // 发型(男) + // 定义发型列表-男生 + val hairstyleForMans = mutableListOf().apply { + add(HairstyleBean(index = 0, id = 7, icon = R.mipmap.ic_cutout_color_less, hairstyle = null, title = "无")) + add(HairstyleBean(index = 1, id = 8, icon = R.mipmap.ic_hairstyle_man_1, hairstyle = R.mipmap.ic_hairstyle_man_1, title = "青春")) + add(HairstyleBean(index = 2, id = 9, icon = R.mipmap.ic_hairstyle_man_2, hairstyle = R.mipmap.ic_hairstyle_man_2, title = "学生")) + add(HairstyleBean(index = 3, id = 10, icon = R.mipmap.ic_hairstyle_man_3, hairstyle = R.mipmap.ic_hairstyle_man_3, title = "职场")) + add(HairstyleBean(index = 4, id = 11, icon = R.mipmap.ic_hairstyle_man_4, hairstyle = R.mipmap.ic_hairstyle_man_4, title = "休闲")) + add(HairstyleBean(index = 5, id = 12, icon = R.mipmap.ic_hairstyle_man_5, hairstyle = R.mipmap.ic_hairstyle_man_5, title = "日韩")) + add(HairstyleBean(index = 6, id = 13, icon = R.mipmap.ic_hairstyle_man_6, hairstyle = R.mipmap.ic_hairstyle_man_6, title = "艺术")) + } + + // 定义尺寸列表 + val resizes = mutableListOf().apply { + add(ResizeBean(id = 0, size = "23x35mm", title = "标准一寸", width = 25f, height = 35f)) + add(ResizeBean(id = 1, size = "22x23mm", title = "小一寸", width = 22f, height = 32f)) + add(ResizeBean(id = 2, size = "35x53mm", title = "标准二寸", width = 35f, height = 53f)) + add(ResizeBean(id = 3, size = "33x48mm", title = "小二寸", width = 33f, height = 48f)) + add(ResizeBean(id = 4, size = "26x32mm", title = "身份证", width = 26f, height = 32f)) + add(ResizeBean(id = 5, size = "33x48mm", title = "护照", width = 33f, height = 48f)) + add(ResizeBean(id = 6, size = "22x32mm", title = "驾驶证", width = 22f, height = 32f)) + add(ResizeBean(id = 7, size = "26x32mm", title = "社保卡", width = 26f, height = 32f)) + add(ResizeBean(id = 8, size = "26x32mm", title = "四六级", width = 26f, height = 32f)) + add(ResizeBean(id = 9, size = "33x48mm", title = "司法考试", width = 33f, height = 48f)) + add(ResizeBean(id = 10, size = "40x60mm", title = "结婚证", width = 40f, height = 60f)) + add(ResizeBean(id = 11, size = "35x45mm", title = "中国签证", width = 35f, height = 45f)) + } + + // 定义尺寸列表(与您提供的示例完全匹配) + val formats = mutableListOf().apply { + add(FormatBean(id = 0, title = "JPG")) + add(FormatBean(id = 1, title = "PNG")) + add(FormatBean(id = 2, title = "GIF")) + add(FormatBean(id = 3, title = "SVG")) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/LoginPage.kt b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt new file mode 100644 index 0000000..877d825 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt @@ -0,0 +1,1178 @@ +@file:Suppress("SameParameterValue") + +package com.img.rabbit.pages + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.util.Log +import android.view.LayoutInflater +import android.widget.CheckBox +import android.widget.TextView +import android.widget.Toast +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf +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.scale +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import com.img.rabbit.R +import com.img.rabbit.utils.AgreementTextHelper +import com.img.rabbit.viewmodel.GeneralViewModel +import com.img.rabbit.viewmodel.LoginViewModel +import com.g.gysdk.EloginActivityParam +import com.g.gysdk.GYManager +import com.g.gysdk.GYResponse +import com.g.gysdk.GyCallBack +import com.img.rabbit.config.Common.agreementUrl +import com.img.rabbit.config.Common.privacyUrl +import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.utils.UrlLinkUtils.openAgreement +import kotlinx.coroutines.delay + + +@Composable +fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel) { + val context = LocalContext.current + val scale = remember { Animatable(0f) } + val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true) + var showNetworkDisconnected by remember { mutableStateOf(false) } + // 网络状态监听 + LaunchedEffect(networkStatus) { + if (!networkStatus) { + // 网络断开时的处理 + Log.w("NetworkStatus","网络断开") + }else{ + Log.w("NetworkStatus","网络已连接") + } + } + Scaffold{ + + Box( + modifier = Modifier.fillMaxSize() + ) { + Image( + painter = painterResource(id = R.mipmap.ic_main_previous_mask), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + + // 顶部栏 + TitleBar(navController = navController, paddingValues = it, title = "", showSave = false) + + Column( + modifier = Modifier + .fillMaxSize() + ) { + when (loginViewModel.loginScreenType.value) { + LoginScreenType.LOGIN_ONE_KEY -> { + Box( + modifier = Modifier.fillMaxSize() + ) { + // 检验是否有一键登录权限成功,显示一键登录页 + OneKeyLoginScreen(context, loginViewModel, generalViewModel) + + // 其他登录方式 + Column ( + modifier = Modifier + .fillMaxSize() + .padding(top = 27.dp), + verticalArrangement = Arrangement.Bottom + ){ + OtherLoginBar(context = context, viewModel = loginViewModel) + } + } + } + LoginScreenType.LOGIN_CAPTCHA -> { + Box( + modifier = Modifier.fillMaxSize() + ) { + // 显示验证码登录页 + CaptchaLoginScreen(context, loginViewModel, generalViewModel) + + + // 其他登录方式 + Column ( + modifier = Modifier + .fillMaxSize() + .padding(top = 27.dp), + verticalArrangement = Arrangement.Bottom + ){ + OtherLoginBar(context = context, viewModel = loginViewModel) + } + } + } + else -> { + // 显示隐私协议政策(同意后才能继续登录) + PrivacyPolicyScreen(viewModel = loginViewModel) { //isAllowPrivacyPolicy -> + loginViewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> + if (isAllowShowOneKeyScreen) { + loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY + } else { + // 检验是否有一键登录权限失败,显示验证码登录 + loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA + } + } + /* + if (isAllowPrivacyPolicy) { + // 同意隐私协议政策,检验是否有一键登录权限 + viewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> + if (isAllowShowOneKeyScreen) { + viewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY + } else { + // 检验是否有一键登录权限失败,显示验证码登录 + viewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA + } + } + } else { + // 不同意隐私协议政策,直接退出应用 + (context as MainActivity).onBackPressed() + } + */ + } + } + } + } + + LaunchedEffect(networkStatus) { + delay(1000L) + showNetworkDisconnected = true + } + if(showNetworkDisconnected){ + if(!networkStatus){ + NetworkDisconnectedPage(onNetworkStatus = { + if(it){ + Toast.makeText(context, "网络已连接", Toast.LENGTH_SHORT).show() + }else{ + Toast.makeText(context, "网络已断开", Toast.LENGTH_SHORT).show() + } + generalViewModel.setNetworkStatus(it) + }) + } + } + + } + + } +} + + +@Composable +private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (Boolean) -> Unit) { + val context = LocalContext.current + Box( + modifier = Modifier.fillMaxSize() + ){ + // 其他登录方式 + Column ( + modifier = Modifier + .fillMaxSize() + .padding(top = 27.dp), + verticalArrangement = Arrangement.Bottom + ){ + OtherLoginBar(context = context, viewModel = viewModel) + } + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xCC000000)) + ){ + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 38.dp, vertical = 213.dp) + .align(Alignment.Center) + .background(Color.White, shape = RoundedCornerShape(26.dp)) + ){ + Image( + painter = painterResource(id = R.mipmap.ic_privacy_policy_top_mask), + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + alignment = Alignment.TopCenter + ) + Text( + text = "用户协议与隐私政策", + modifier = Modifier + .padding(top = 54.dp) + .align(Alignment.TopCenter), + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = Color(0xFF1A1A1A) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .align(Alignment.BottomCenter) + ) { + //同意按钮,用户协议与隐私政策 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onAgreementChange(true) + viewModel.setIsPolicyAgreement(true) + } + ) { + Text( + "同意", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + //不同按钮,意用户协议与隐私政策 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp) + .background( + Color(0x00000000), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onAgreementChange(false) + viewModel.setIsPolicyAgreement(false) + } + ) { + Text( + "不同意", + color = Color(0xFFAAAAAA), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + } + } + } + } +} + +/** + * 验证码登录 + */ +@Composable +private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, generalViewModel: GeneralViewModel) { + val gradientBrush = Brush.verticalGradient( + colors = listOf( + Color(0xFF91FEFA), // 浅蓝色 + Color(0x0091FEFA) // 半透明 + ), + startY = 0f, + endY = Float.POSITIVE_INFINITY + ) + // 验证码倒计时 + var isCaptchaCountdown by remember { mutableStateOf(false) } + // 倒计时秒数 + var captchaCountdown by remember { mutableIntStateOf(60) } + + if (isCaptchaCountdown) { + LaunchedEffect(key1 = isCaptchaCountdown) { + while (captchaCountdown > 0) { + delay(1000) // 等待1秒 + captchaCountdown-- + } + isCaptchaCountdown = false + captchaCountdown = 60 // 重置倒计时 + } + } + + Column { + //登录标签 + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 38.dp, end = 38.dp, top = 150.dp) + ) { + Box( + modifier = Modifier + .width(4.dp) + .height(55.dp) + .background(gradientBrush) + ) + Column() { + Text( + text = "欢迎登录", + modifier = Modifier.padding(start = 12.dp), + fontSize =18.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF000000) + ) + Text( + text = stringResource(R.string.app_name), + modifier = Modifier.padding(start = 12.dp), + fontSize =32.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF000000) + ) + } + } + //登录框 + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 38.dp, end = 38.dp, top = 80.dp) + ) { + // 用户名框(手机号) + Box( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .background( + Color(0x4DE3E0ED), + shape = RoundedCornerShape(12.dp), + ) + ){ + Row( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + ) { + Text( + text = "+86", + modifier = Modifier + .padding(start = 14.dp, end = 12.dp) + .align(Alignment.CenterVertically), + fontSize =14.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF3D3D3D) + ) + + Image( + painter = painterResource(id = R.mipmap.ic_vertical_divider_dotted_line), + contentDescription = null, + modifier = Modifier + .height(29.dp) + .width(1.dp) + .align(Alignment.CenterVertically) + ) + + BasicTextField( + value = viewModel.userName.value, + onValueChange = { viewModel.setUserName(it) }, + modifier = Modifier + .fillMaxWidth() + .background(Color.Transparent) + .padding(horizontal = 12.dp) + .align(Alignment.CenterVertically), + textStyle = androidx.compose.ui.text.TextStyle( + color = Color.Black, + fontSize = 16.sp + ), + singleLine = true, + maxLines = 1, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + keyboardType = androidx.compose.ui.text.input.KeyboardType.Phone, + imeAction = androidx.compose.ui.text.input.ImeAction.Done + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color.Transparent) + ) { + if (viewModel.userName.value.isEmpty()) { + Text( + "请输入手机号", + color = Color.Gray, + fontSize = 16.sp + ) + } + innerTextField() + } + }, + ) + } + } + // 分割线 + Box( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .background(Color.Transparent) + ) + // 验证码框 + Box( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .background( + Color(0x4DE3E0ED), + shape = RoundedCornerShape(12.dp), + ) + ) { + Row( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .padding(end = 5.dp), + horizontalArrangement = Arrangement.End + + ) { + + BasicTextField( + value = viewModel.captcha.value, + onValueChange = { viewModel.setCaptcha(it) }, + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .background(Color.Transparent), + textStyle = androidx.compose.ui.text.TextStyle( + color = Color.Black, + fontSize = 16.sp + ), + singleLine = true, + maxLines = 1, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + keyboardType = androidx.compose.ui.text.input.KeyboardType.Text, + imeAction = androidx.compose.ui.text.input.ImeAction.Done + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically) + .padding(horizontal = 14.dp) + .background(Color.Transparent) + ) { + if (viewModel.captcha.value.isEmpty()) { + Text( + "请输入验证码", + color = Color.Gray, + fontSize = 16.sp + ) + } + innerTextField() + } + } + ) + + Box( + modifier = Modifier + .height(40.dp) + .width(98.dp) + .align(Alignment.CenterVertically) + .background( + if (isCaptchaCountdown) Color(0xFFE0E0E0) else Color(0xFFFFFFFF), + shape = RoundedCornerShape(8.dp) + ) + .clickable(enabled = !isCaptchaCountdown) { + // 点击获取验证码 + if (validatePhoneEmpty( + context = context, + viewModel = viewModel, + showToast = true + ) + ) { + //TODO 请求验证码(请完善requestCaptcha函数) + viewModel.requestCaptcha() + + // 开始倒计时(倒计时应该在requestCaptcha完成后开始) + isCaptchaCountdown = true + return@clickable + } + } + ){ + if (isCaptchaCountdown) { + // 倒计时文本(倒计时从60秒开始) + Text( + "${captchaCountdown}秒", + color = Color(0xFF3D3D3D), + fontSize = 12.sp, + modifier = Modifier + .align(Alignment.Center) + ) + } else { + Text( + "获取验证码", + color = Color(0xFF3D3D3D), + fontSize = 12.sp, + modifier = Modifier + .align(Alignment.Center) + ) + } + } + } + + } + + } + + // 登录按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 46.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 点击登录 + if (validateCaptchaLoginEmpty( + context = context, + viewModel = viewModel, + showToast = true + ) + ) { + //TODO 验证码登录请求 + Toast.makeText(context, "登录成功!", Toast.LENGTH_SHORT).show() + //TODO 登录成功后,保存 token + generalViewModel.kv.encode("token", "123232123231231") + // 登录成功后,设置登录状态为 true + viewModel.setLogin(true) + } + } + ) { + Text( + "登录", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 14.dp) + ) { + Checkbox( + checked = viewModel.isPolicyAgreement.value, + onCheckedChange = { isChecked -> + viewModel.setIsPolicyAgreement(isChecked) + }, + modifier = Modifier + .size(16.dp) + .scale(0.35f) + .padding(start = 6.dp) + .background( + if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color.Transparent, + shape = RoundedCornerShape(36.dp) + ) + .border( + width = 1.dp, + color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color(0xFFCCCCCC), + shape = RoundedCornerShape(36.dp) + ) + .align(Alignment.CenterVertically), + colors = androidx.compose.material3.CheckboxDefaults.colors( + checkedColor = Color.Transparent, // 隐藏默认背景 + uncheckedColor = Color.Transparent, // 隐藏默认背景 + checkmarkColor = Color.White + ) + + ) + val annotatedText = buildAnnotatedString { + append("我已阅读并同意") + + // 用户协议部分 + pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《用户协议》") + } + pop() + + append("和") + + // 隐私政策部分 + pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《隐私政策》") + } + pop() + } + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations(offset, offset) + .firstOrNull()?.let { annotation -> + when (annotation.tag) { + "USER_AGREEMENT" -> { + // 打开用户协议 + openAgreement(context, agreementUrl) + } + "PRIVACY_POLICY" -> { + // 打开隐私政策 + openAgreement(context, privacyUrl) + } + } + } + }, + style = androidx.compose.ui.text.TextStyle( + fontSize = 12.sp, + color = Color.Gray + ), + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically) + ) + } + } +} +/** + * 一键登录页 + * 至少包含号码栏(NumberTextview)、品牌露出(SloganTextview)、登录按钮(LoginButton)、隐私确认(PrivacyCheckbox)、隐私标题(PrivacyTextview) + */ +@SuppressLint("UseKtx", "InflateParams", "SetTextI18n") +@Composable +private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, generalViewModel: GeneralViewModel) { + val ONEKEY_TAG = "OneKeyLoginScreen" + + val preLoginResult = GYManager.getInstance().preLoginResult + val phoneNumber = viewModel.oneKeyPreLogin?.number ?: "" + val operator = preLoginResult.operator//运营商,CM 移动,CT 电信,CU 联通 + val privacyName = "《${preLoginResult.privacyName}》" + val privacyUrl = preLoginResult.privacyUrl + + // 详细打印preLoginResult信息 + Log.w(ONEKEY_TAG, "=== preLoginResult 详细信息 ===") + Log.w(ONEKEY_TAG, "preLoginResult对象: $preLoginResult") + Log.w(ONEKEY_TAG, "operator: ${preLoginResult.operator}") + Log.w(ONEKEY_TAG, "isValid: ${preLoginResult.isValid}") + Log.w(ONEKEY_TAG, "privacyName: ${preLoginResult.privacyName}") + Log.w(ONEKEY_TAG, "privacyUrl: ${preLoginResult.privacyUrl}") + Log.w(ONEKEY_TAG, "================================") + + + val agreementText = "登录即认可${privacyName}、《用户协议》和《隐私政策》并使用本机号码登录" + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + // 使用AndroidView嵌入XML布局 + @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") + AndroidView( + factory = { context -> + LayoutInflater.from(context).inflate( + R.layout.layout_one_key_login, + null, + false + ) as ConstraintLayout + }, + modifier = Modifier.fillMaxSize(), + update = { view -> + // 动态更新XML布局中的内容 + val phoneTextView = view.findViewById(R.id.layout_one_key_login_tv_phone) + phoneTextView.text = phoneNumber // 需要回去手机号码 + + val serviceTextView = view.findViewById(R.id.layout_one_key_login_tv_service) + val serviceText = when (operator) { + "CT" -> {//运营商,CM 移动,CT 电信,CU 联通 + "天翼账号提供认证服务" + } + "CU" -> {//联通 + "联通账号提供认证服务" + } + else -> {//移动 + "移动账号提供认证服务" + } + } + serviceTextView.text = serviceText // 品牌露出,如:“天翼账号提供认证服务” + + val checkbox = view.findViewById(R.id.layout_one_key_login_agreement_checkbox) + checkbox.isChecked = viewModel.isPolicyAgreement.value + checkbox.setOnCheckedChangeListener { _, isChecked -> + viewModel.setIsPolicyAgreement(isChecked) + } + + val agreementTextView = view.findViewById(R.id.layout_one_key_login_agreement_tv) + //TODO 服务协议,如:“登录即认可《天翼账号服务与隐私协议》、《用户协议》和《隐私政策》并使用本机号码登录” + + val targets = mapOf( + "serviceAgreement" to privacyName, + "userAgreement" to "《用户协议》", + "privacyAgreement" to "《隐私政策》" + ) + // 设置可点击的协议文本 + AgreementTextHelper.setupAgreementTextView(agreementText, targets, agreementTextView, isUnderlineText = false) { agreementType -> + when (agreementType) { + "serviceAgreement" -> openAgreement(context, privacyUrl) + "userAgreement" -> openAgreement(context, agreementUrl) + "privacyAgreement" -> openAgreement(context, privacyUrl) + } + } + + val loginButton = view.findViewById(R.id.layout_one_key_login_btn) + loginButton.setOnClickListener { + // 处理登录点击 + oneKeyLogin( + context = context, + numberTv = phoneTextView, + sloganTv = serviceTextView, + loginBtn = loginButton, + checkBox = checkbox, + privacyTv = agreementTextView, + viewModel = viewModel, + generalViewModel = generalViewModel + ) + } + } + ) + } +} + +/** + * 一键登录页 + * 至少包含号码栏(NumberTextview)、品牌露出(SloganTextview)、登录按钮(LoginButton)、隐私确认(PrivacyCheckbox)、隐私标题(PrivacyTextview) + */ +/* +@SuppressLint("UseKtx") +@Composable +private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel) { + val preLoginResult = GYManager.getInstance().preLoginResult + preLoginResult.operator + preLoginResult.isValid + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 230.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "18698756851", + fontWeight = FontWeight.Bold, + fontSize = 36.sp, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + ) + Text( + text = "天翼账号提供认证服务", + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Color(0xFF767676), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + ) + + // 登录按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 46.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable { + // 点击登录 + if (validateCaptchaLoginEmpty( + context = context, + viewModel = viewModel, + showToast = true + ) + ) { + //TODO 验证码登录请求 + Toast.makeText(context, "登录成功!", Toast.LENGTH_SHORT).show() + } + } + ) { + Text( + "本机号码一键绑定", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 14.dp) + ) { + Checkbox( + checked = viewModel.isPolicyAgreement.value, + onCheckedChange = { isChecked -> + viewModel.setIsPolicyAgreement(isChecked) + }, + modifier = Modifier + .size(16.dp) + .scale(0.35f) + .padding(top = 16.dp, start = 6.dp) + .background( + if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color.Transparent, + shape = RoundedCornerShape(36.dp) + ) + .border( + width = 1.dp, + color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color(0xFFCCCCCC), + shape = RoundedCornerShape(36.dp) + ), + colors = androidx.compose.material3.CheckboxDefaults.colors( + checkedColor = Color.Transparent, // 隐藏默认背景 + uncheckedColor = Color.Transparent, // 隐藏默认背景 + checkmarkColor = Color.White + ) + + ) + val annotatedText = buildAnnotatedString { + append("我已阅读并同意") + + // 用户协议部分 + pushStringAnnotation(tag = "SERVICE_AGREEMENT", annotation = "service_agreement") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("${preLoginResult.privacyName}") + } + pop() + + append("、") + + // 用户协议部分 + pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《用户协议》") + } + pop() + + append("和") + + // 隐私政策部分 + pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《隐私政策》") + } + pop() + } + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations(offset, offset) + .firstOrNull()?.let { annotation -> + when (annotation.tag) { + "SERVICE_AGREEMENT" -> { + // 打开服务协议 + preLoginResult.privacyUrl?.let { + Intent(Intent.ACTION_VIEW, it.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }.let { intent -> + context.startActivity(intent) + } + } + } + "USER_AGREEMENT" -> { + //TODO 打开用户协议 + Toast.makeText(context, "打开用户协议", Toast.LENGTH_SHORT).show() + } + "PRIVACY_POLICY" -> { + //TODO 打开隐私政策 + Toast.makeText(context, "打开隐私政策", Toast.LENGTH_SHORT).show() + } + } + } + }, + style = androidx.compose.ui.text.TextStyle( + fontSize = 12.sp, + color = Color.Gray + ), + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically) + ) + } + } +} + */ + +@Composable +private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) { + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 64.dp, end = 64.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box( + modifier = Modifier + .width(60.dp) + .height(2.dp) + .align(Alignment.CenterVertically) + .background( + brush = Brush.horizontalGradient( + colors = listOf(Color(0x00D8D8D8), Color(0xFFE5E5E5)) + ) + ) + ) + Text( + "其他登录方式", + color = Color(0xFF767676), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .padding(horizontal = 14.dp) + .align(Alignment.CenterVertically) + ) + Box( + modifier = Modifier + .width(60.dp) + .height(2.dp) + .align(Alignment.CenterVertically) + .background( + brush = Brush.horizontalGradient( + colors = listOf(Color(0xFFE5E5E5), Color(0x00D8D8D8)) + ) + ) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 6.dp, vertical = 27.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Image( + painter = painterResource(id = R.mipmap.ic_wechat), + contentDescription = null, + modifier = Modifier + .size(32.dp) + .weight(1f) + .align(Alignment.CenterVertically) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 打开微信登录 + Toast.makeText(context, "打开微信登录", Toast.LENGTH_SHORT).show() + } + ) + Image( + painter = painterResource(id = R.mipmap.ic_alipay), + contentDescription = null, + modifier = Modifier + .size(32.dp) + .weight(1f) + .align(Alignment.CenterVertically) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 打开支付宝登录 + Toast.makeText(context, "打开支付宝登录", Toast.LENGTH_SHORT).show() + } + ) + Image( + painter = painterResource(id = R.mipmap.ic_phone), + contentDescription = null, + modifier = Modifier + .size(32.dp) + .weight(1f) + .align(Alignment.CenterVertically) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 校验码登录 + viewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA + } + ) + } + } +} + +/** + * 验证手机号是否为空 + */ +private fun validatePhoneEmpty(context: Context, viewModel: LoginViewModel, showToast: Boolean = false): Boolean { + if (showToast && viewModel.userName.value.isEmpty()) { + Toast.makeText(context, "请输入手机号", Toast.LENGTH_SHORT).show() + } + return viewModel.userName.value.isNotEmpty() +} + +/** + * 验证验证码登录是否为空 + */ +private fun validateCaptchaLoginEmpty(context: Context, viewModel: LoginViewModel, showToast: Boolean = false): Boolean { + if (showToast) { + if(viewModel.userName.value.isEmpty()){ + Toast.makeText(context, "请输入手机号", Toast.LENGTH_SHORT).show() + }else if(viewModel.captcha.value.isEmpty()){ + Toast.makeText(context, "请输入验证码", Toast.LENGTH_SHORT).show() + }else if(!viewModel.isPolicyAgreement.value){ + Toast.makeText(context, "请同意用户协议和隐私政策", Toast.LENGTH_SHORT).show() + } + } + return viewModel.userName.value.isNotEmpty() && viewModel.captcha.value.isNotEmpty() && viewModel.isPolicyAgreement.value +} + +private fun oneKeyLogin( + context: Context, + numberTv: TextView, + sloganTv: TextView, + loginBtn: TextView, + checkBox: CheckBox, + privacyTv: TextView, + viewModel: LoginViewModel, + generalViewModel: GeneralViewModel +) { + val eloginActivityParam = EloginActivityParam() + .setActivity(context as Activity) + .setNumberTextview(numberTv) + .setSloganTextview(sloganTv) + .setLoginButton(loginBtn) + .setPrivacyCheckbox(checkBox) + .setPrivacyTextview(privacyTv) + .setUiErrorListener { msg -> //隐私协议未打勾、界面不合规、setLoginOnClickListener抛出异常等情况下的回调 + Log.e("OneKeyLogin", "UIErrorListener.onError:$msg") + } + .setLoginOnClickListener { + if (!checkBox.isChecked) { + // 抛出异常,避免sdk进行后续登录动作(否则eAccountLogin会回调onFailed错误) + throw IllegalStateException("请先仔细阅读协议并勾选,然后再点击登录") + } + //启动登录时候的转圈圈 + } + GYManager.getInstance().eAccountLogin(eloginActivityParam, 5000, object : GyCallBack { + override fun onSuccess(response: GYResponse?) { + //TODO 登录成功,需要与后端交互 + Log.i("OneKeyLogin", "onSuccess:$response") + //TODO 登录成功后,保存 token + generalViewModel.kv.encode("token", "123232123231231") + viewModel.setLogin(true) + } + + override fun onFailed(p0: GYResponse?) { + //TODO 登录失败 + Log.e("OneKeyLogin", "onFailed:$p0") + } + }) +} + + +enum class LoginScreenType { + LOGIN_NORMAL, + LOGIN_ONE_KEY, + LOGIN_CAPTCHA, +} + +@Preview +@Composable +private fun PreviewOneKeyLoginScreen() { + OneKeyLoginScreen(LocalContext.current, viewModel(), viewModel()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/MainPage.kt b/app/src/main/java/com/img/rabbit/pages/MainPage.kt new file mode 100644 index 0000000..f8dca7d --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/MainPage.kt @@ -0,0 +1,217 @@ +package com.img.rabbit.pages + +import android.annotation.SuppressLint +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.pages.screen.mine.FeedbackScreen +import com.img.rabbit.pages.screen.mine.OnlineServiceScreen +import com.img.rabbit.pages.screen.mine.SettingScreen +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +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.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.img.rabbit.R +import com.img.rabbit.pages.screen.HomeScreen +import com.img.rabbit.pages.screen.MineScreen +import com.img.rabbit.pages.screen.make.CutoutScreen +import com.img.rabbit.pages.screen.make.FormatScreen +import com.img.rabbit.pages.screen.make.LongImageScreen +import com.img.rabbit.pages.screen.make.ResizeScreen +import com.img.rabbit.pages.screen.mine.setting.AboutScreen +import com.img.rabbit.pages.screen.mine.setting.AccountBindScreen +import com.img.rabbit.pages.screen.mine.setting.AccountManagerScreen +import com.img.rabbit.pages.screen.other.CameraGuideScreen +import com.img.rabbit.route.ScreenRoute +import com.img.rabbit.viewmodel.GeneralViewModel +import com.img.rabbit.viewmodel.LoginViewModel + +// 定义底部导航的标签页 +sealed class TabItem(val title: String, val normalIconRes: Int, val selectedIconRes: Int, val normalColor: Color, val selectedColor: Color) { + object Home : TabItem("首页", R.mipmap.ic_home_normal, R.mipmap.ic_home_selected, Color(0xFFAAAAAA), Color(0xFF1A1A1A)) + object Mine : TabItem("我的", R.mipmap.ic_mine_normal, R.mipmap.ic_mine_selected, Color(0xFFAAAAAA), Color(0xFF1A1A1A)) +} + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel) { + val navController = rememberNavController() + val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true) + val isNavigationBarVisible by generalViewModel.isNavigationBarVisible.observeAsState(initial = true) + val tabItems = listOf( + TabItem.Home, + TabItem.Mine + ) + var selectedTab: TabItem by remember { mutableStateOf(TabItem.Home) } + + // 监听返回事件 + val currentBackStackEntry = navController.currentBackStackEntry + + LaunchedEffect(currentBackStackEntry) { + // 当返回到MineScreen页面时执行的操作 + if (currentBackStackEntry?.destination?.route == "home") { + // 显示TabBar + generalViewModel.setNavigationBarVisible(true) + } + } + + // 网络状态监听 + LaunchedEffect(networkStatus) { + if (!networkStatus) { + // 网络断开时的处理 + Log.w("NetworkStatus","网络断开") + }else{ + Log.w("NetworkStatus","网络已连接") + } + } + + Scaffold( + bottomBar = { + if (isNavigationBarVisible) { + Box { + NavigationBar( + containerColor = Color.White, + contentColor = Color.Transparent + ) { + tabItems.forEachIndexed { index, item -> + val iconRes = if (selectedTab == tabItems[index]) { + item.selectedIconRes + } else { + item.normalIconRes + } + NavigationBarItem( + icon = { + Icon( + painter = painterResource(id = iconRes), + contentDescription = item.title, + tint = Color.Unspecified + ) + }, + label = { Text(item.title, color = if (selectedTab == tabItems[index]) item.selectedColor else item.normalColor) }, + selected = selectedTab == tabItems[index], + onClick = { selectedTab = tabItems[index] }, + colors = NavigationBarItemDefaults.colors( + indicatorColor = Color.Transparent + ) + ) + } + } + // 顶部横线 + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(Color.Gray.copy(alpha = 0.3f)) + ) + } + } + } + ) { + Box( + modifier = Modifier.fillMaxSize() + ) { + // 导航主机 + NavHost( + navController = navController, + startDestination = ScreenRoute.Home.route + ) { + // Tab页面 + composable(ScreenRoute.Home.route) { + HomeScreen( + navController = navController, + generalViewModel = generalViewModel + ) + } + composable(ScreenRoute.Mine.route) { + MineScreen( + navController = navController, + generalViewModel = generalViewModel + ) + } + // 抠图页面(Cutout) + composable(ScreenRoute.Cutout.route) { + CutoutScreen(navController = navController) + } + // 证件页面(Certificate) + composable(ScreenRoute.Resize.route) { + ResizeScreen(navController = navController) + } + // 格式页面(Format) + composable(ScreenRoute.Format.route) { + FormatScreen(navController = navController) + } + // 拍照指南页面(CameraGuide) + composable(ScreenRoute.CameraGuide.route) { + CameraGuideScreen(navController = navController) + } + // 长图页面(LongImage) + composable(ScreenRoute.LongImage.route) { + LongImageScreen(navController = navController) + } + + + // 我的页面(Mine) + composable(ScreenRoute.Feedback.route) { + FeedbackScreen(navController = navController) + } + composable(ScreenRoute.OnlineService.route) { + OnlineServiceScreen(navController = navController) + } + composable(ScreenRoute.Setting.route) { + SettingScreen(navController = navController) + } + + // 设置页面(Setting) + composable(ScreenRoute.BindAccount.route) { + AccountBindScreen(navController = navController) + } + composable(ScreenRoute.ManagerAccount.route) { + AccountManagerScreen(navController = navController) + } + composable(ScreenRoute.AboutMine.route) { + AboutScreen(navController = navController) + } + + // 登录页面(Login) + composable(ScreenRoute.Login.route) { + LoginScreen( + navController = navController, + generalViewModel = generalViewModel, + loginViewModel = loginViewModel + ) + } + } + + // 根据选中的Tab切换导航路由 + LaunchedEffect(selectedTab) { + when (selectedTab) { + TabItem.Home -> navController.navigate(ScreenRoute.Home.route) { + popUpTo(ScreenRoute.Home.route) { inclusive = true } + } + TabItem.Mine -> navController.navigate(ScreenRoute.Mine.route) { + popUpTo(ScreenRoute.Mine.route) { inclusive = true } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/NetworkDisconnectedPage.kt b/app/src/main/java/com/img/rabbit/pages/NetworkDisconnectedPage.kt new file mode 100644 index 0000000..1d28f05 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/NetworkDisconnectedPage.kt @@ -0,0 +1,147 @@ +package com.img.rabbit.pages + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.img.rabbit.R +import com.img.rabbit.utils.NetworkStatus + +@Composable +fun NetworkDisconnectedPage(onNetworkStatus: (Boolean) -> Unit) { + val context = LocalContext.current + val scale = remember { Animatable(0f) } + + Box( + modifier = Modifier.fillMaxSize().background(color = Color.White) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_normal_top_mask), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + + /* + // 返回按钮 + Image( + painter = painterResource(id = R.mipmap.ic_back), + contentDescription = null, + modifier = Modifier + .padding(top = 56.dp, start = 16.dp) + .wrapContentSize() + .clickable { + // 点击返回按钮,返回上一页 + (context as MainActivity).onBackPressed() + //navController.popBackStack() + } + ) + */ + + Column( + modifier = Modifier + .fillMaxSize() + ) { + + Image( + painter = painterResource(id = R.mipmap.ic_network_disconnected), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 253.dp) + ) + Text( + text = "网络连接断开,请检查网络设置", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + .padding(top = 23.dp) + ) + + Text( + text = "刷新页面", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + .padding(top = 57.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 点击刷新按钮,刷新页面 + onNetworkStatus(NetworkStatus.isNetworkAvailable(context)) + } + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 71.dp) + .align(Alignment.CenterHorizontally) + .padding(top = 11.dp) + .background(color = Color(0xFF252525), shape = RoundedCornerShape(359.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 点击开启网络权限按钮,跳转至系统设置页面 + NetworkStatus.openNetworkSettings(context) + } + ){ + Text( + text = "开启网络权限", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFFC2FF43), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + .padding(top = 12.dp, bottom = 12.dp) + ) + } + + } + } + +} + +@Preview +@Composable +private fun PreviewNetworkDisconnectedPage() { + NetworkDisconnectedPage(onNetworkStatus = {}) +} diff --git a/app/src/main/java/com/img/rabbit/pages/screen/HomeScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/HomeScreen.kt new file mode 100644 index 0000000..ec496d4 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/HomeScreen.kt @@ -0,0 +1,883 @@ +package com.img.rabbit.pages.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R +import com.img.rabbit.route.ScreenRoute +import com.img.rabbit.viewmodel.GeneralViewModel + +@Composable +fun HomeScreen(navController: NavHostController,generalViewModel: GeneralViewModel) { + val context = LocalContext.current + val scrollState = rememberScrollState() + + // 监听返回事件 + val currentBackStackEntry = navController.currentBackStackEntry + + LaunchedEffect(currentBackStackEntry) { + // 当返回到MineScreen页面时执行的操作 + if (currentBackStackEntry?.destination?.route == "home") { + // 显示TabBar + generalViewModel.setNavigationBarVisible(true) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + ){ + Image( + painter = painterResource(id = R.mipmap.ic_home_top_mask), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + Column ( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .offset(y = (-17).dp) + .padding(bottom = 56.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) + .verticalScroll(scrollState) + ){ + //选尺寸制作 + Image( + painter = painterResource(id = R.mipmap.ic_home_title_1_size), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .wrapContentWidth() + .padding(start = 16.dp, end = 16.dp, top = 12.dp) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 12.dp) + ) { + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 25f) + currentBackStackEntry?.savedStateHandle?.set("height", 35f) + navigate(ScreenRoute.Cutout.route) + } + } + ){ + Image( + painter = painterResource(id = R.mipmap.ic_home_size_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "标准一寸", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "25x35mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF767676), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(10.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 22f) + currentBackStackEntry?.savedStateHandle?.set("height", 32f) + navigate(ScreenRoute.Cutout.route) + } + } + ){ + Image( + painter = painterResource(id = R.mipmap.ic_home_size_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "小一寸", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "22x32mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF767676), + modifier = Modifier.wrapContentSize() + ) + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 12.dp) + ) { + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 35f) + currentBackStackEntry?.savedStateHandle?.set("height", 53f) + navigate(ScreenRoute.Cutout.route) + } + } + ){ + Image( + painter = painterResource(id = R.mipmap.ic_home_size_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "标准二寸", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "35x53mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF767676), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(10.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 33f) + currentBackStackEntry?.savedStateHandle?.set("height", 48f) + navigate(ScreenRoute.Cutout.route) + } + } + ){ + Image( + painter = painterResource(id = R.mipmap.ic_home_size_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "小二寸", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "33x48mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF767676), + modifier = Modifier.wrapContentSize() + ) + } + } + } + + + //证据制作 + Image( + painter = painterResource(id = R.mipmap.ic_home_title_2_certificate), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .wrapContentWidth() + .padding(start = 16.dp, end = 16.dp, top = 18.dp) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 12.dp) + ){ + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 26f) + currentBackStackEntry?.savedStateHandle?.set("height", 32f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "身份证", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "26x32mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 33f) + currentBackStackEntry?.savedStateHandle?.set("height", 48f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "护照", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "33x48mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 22f) + currentBackStackEntry?.savedStateHandle?.set("height", 32f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "驾驶证", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "22x32mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 26f) + currentBackStackEntry?.savedStateHandle?.set("height", 32f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "社保卡", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "26x32mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 12.dp) + ){ + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 26f) + currentBackStackEntry?.savedStateHandle?.set("height", 32f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "四六级", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "26x32mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 33f) + currentBackStackEntry?.savedStateHandle?.set("height", 48f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "司法考试", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "33x48mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 40f) + currentBackStackEntry?.savedStateHandle?.set("height", 60f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "结婚证", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "40x60mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转抠图页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 35f) + currentBackStackEntry?.savedStateHandle?.set("height", 45f) + navigate(ScreenRoute.Cutout.route) + } + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_certificate_bg), + contentDescription = null, + ) + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Text( + text = "中国签证", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "35x45mm", + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFAAAAAA), + modifier = Modifier.wrapContentSize() + ) + } + } + } + + + //其他 + Image( + painter = painterResource(id = R.mipmap.ic_home_title_3_other), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .wrapContentWidth() + .padding(start = 16.dp, end = 16.dp, top = 18.dp) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 12.dp) + ){ + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转格式页面 + navController.navigate(ScreenRoute.LongImage.route) + } + ) { + + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_other_1_puzzle), + contentDescription = null, + ) + + Text( + text = "拼长图", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize().padding(top = 12.dp) + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转格式页面 + navController.navigate(ScreenRoute.Format.route) + } + ) { + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_other_2_format), + contentDescription = null, + ) + + Text( + text = "格式转换", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize().padding(top = 12.dp) + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转证件页面 + navController.apply { + currentBackStackEntry?.savedStateHandle?.set("width", 25f) + currentBackStackEntry?.savedStateHandle?.set("height", 35f) + navigate(ScreenRoute.Resize.route) + } + } + ) { + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_other_3_size), + contentDescription = null, + ) + + Text( + text = "改尺寸", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize().padding(top = 12.dp) + ) + } + } + Box( + modifier = Modifier + .width(8.dp) + ) + Box( + modifier = Modifier + .wrapContentWidth() + .weight(1f) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转拍照指南页面 + navController.navigate(ScreenRoute.CameraGuide.route) + } + ) { + Column( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_home_other_4_camera), + contentDescription = null, + ) + + Text( + text = "拍照指南", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize().padding(top = 12.dp) + ) + } + } + + } + Box( + modifier = Modifier.fillMaxWidth().height(56.dp) + ) + } + + + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewHomeScreen() { + HomeScreen(navController = rememberNavController(),generalViewModel = viewModel()) +} diff --git a/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt new file mode 100644 index 0000000..51c4310 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt @@ -0,0 +1,437 @@ +package com.img.rabbit.pages.screen + +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R +import com.img.rabbit.viewmodel.GeneralViewModel + +@Composable +fun MineScreen( + navController: NavHostController, + generalViewModel: GeneralViewModel, +) { + val context = LocalContext.current + val vipMember by remember { mutableStateOf(false) } + + // 监听返回事件 + val currentBackStackEntry = navController.currentBackStackEntry + + LaunchedEffect(currentBackStackEntry) { + // 当返回到MineScreen页面时执行的操作 + if (currentBackStackEntry?.destination?.route == "mine") { + // 显示TabBar + generalViewModel.setNavigationBarVisible(true) + } + } + + Box( + modifier = Modifier.fillMaxSize().background(Color(0xFFF9F9F9)) + ){ + Image( + painter = painterResource(id = R.mipmap.ic_mine_top_mask), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, top = 112.dp) + ) { + // 头像和登录/注册 + Row( + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.mipmap.ic_user_avatar_default), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(90.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 处理点击事件 + Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show() + } + ) + Column( + modifier = Modifier + .padding(start = 16.dp) + .align(Alignment.CenterVertically) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转登录页面 + navController.navigate("login") + } + ) { + Text( + text = "登录/注册", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.wrapContentSize() + ) + Text( + text = "登录体验更多功能哦~", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF767676), + modifier = Modifier + .wrapContentSize() + .padding(top = 10.dp) + ) + } + } + + // VIP bar + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 33.dp) + ){ + + if(vipMember){ + Image( + painter = painterResource(id = R.mipmap.ic_mine_vip_bar_bg1), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + }else{ + Image( + painter = painterResource(id = R.mipmap.ic_mine_vip_bar_bg), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + } + + Row( + modifier = Modifier.wrapContentSize() + ) { + Text( + text = if (vipMember) "月度会员" else "开通会员", + style = TextStyle( + brush = Brush.linearGradient( + colors = listOf(Color(0xFFC8FF54), Color(0xFFFFFFFF)) + ), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .wrapContentSize() + .padding(start = 14.dp, top = 10.dp) + ) + Text( + text = if (vipMember) "2025.12.22前会员到期" else "解锁微圈相册全部特权", + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFFFFFFFF), + modifier = Modifier + .wrapContentSize() + .padding(start = 14.dp, top = 10.dp) + .align(Alignment.CenterVertically) + ) + } + } + + if (!vipMember){ + Column( + modifier = Modifier + .fillMaxWidth() + .padding(end = 17.dp) + .offset(y = (-40).dp) + ) { + + Image( + painter = painterResource(id = R.mipmap.ic_mine_open), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .align(Alignment.End) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 处理点击事件 + Toast.makeText(context, "开通会员", Toast.LENGTH_SHORT).show() + } + ) + } + } + + //功能项 + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp) + .background( + Color(0xFFFFFFFF), + RoundedCornerShape(18.dp) + ) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = Color(0x4DE5E5E5), + spotColor = Color(0x4DE5E5E5) + ) + ){ + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转反馈页面 + navController.navigate("feedback") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.mipmap.ic_mine_feedback), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp) + ) + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "意见反馈", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) // 改为end padding + ) + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转在线客服页面 + navController.navigate("onlineService") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.mipmap.ic_mine_service), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp) + ) + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "在线客服", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) // 改为end padding + ) + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 处理点击事件 + Toast.makeText(context, "版本更新", Toast.LENGTH_SHORT).show() + }, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.mipmap.ic_mine_update), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp) + ) + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "版本更新", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) // 改为end padding + ) + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转设置页面 + navController.navigate("setting") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.mipmap.ic_mine_setting), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp) + ) + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "设置", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) // 改为end padding + ) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewMineScreen(){ + MineScreen(navController = rememberNavController(), generalViewModel = viewModel()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt new file mode 100644 index 0000000..d2564f0 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt @@ -0,0 +1,621 @@ +package com.img.rabbit.pages.screen.make + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.toSize +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import coil3.compose.AsyncImage +import com.img.rabbit.R +import com.img.rabbit.bean.ClothingBean +import com.img.rabbit.bean.HairstyleBean +import com.img.rabbit.components.AppearanceType +import com.img.rabbit.components.DrawingBoardPicker +import com.img.rabbit.config.CommonData.clothingForFemales +import com.img.rabbit.config.CommonData.clothingForMans +import com.img.rabbit.config.CommonData.colors +import com.img.rabbit.config.CommonData.hairstyleForFemales +import com.img.rabbit.config.CommonData.hairstyleForMans +import com.img.rabbit.config.CommonData.resizes +import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.utils.ExportFormat +import com.img.rabbit.utils.ImageUtils +import com.img.rabbit.utils.ImageUtils.saveCanvasToGallery +import java.io.File +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import com.img.rabbit.utils.PhotoCutter +import kotlinx.coroutines.launch + + +// 建议的状态数据结构 +data class TransformState( + val offset: Offset = Offset.Zero, + val scale: Float = 1f, + val rotation: Float = 0f +) + +// 服装和发型资源管理类 +object ResourceManager { + // 加载服装资源 + fun loadClothingResource(context: Context, resId: Int): Bitmap? { + return try { + BitmapFactory.decodeResource(context.resources, resId) + } catch (e: Exception) { + Log.e("ResourceManager", "加载服装资源失败: ${e.message}") + null + } + } + + // 加载发型资源 + fun loadHairstyleResource(context: Context, resId: Int): Bitmap? { + return try { + BitmapFactory.decodeResource(context.resources, resId) + } catch (e: Exception) { + Log.e("ResourceManager", "加载发型资源失败: ${e.message}") + null + } + } +} + +@Composable +fun CutoutScreen(navController: NavController) { + val context = LocalContext.current + // 图片显示区域 - 支持头像编辑(用于最终数据的导出保存Bitmap) + val graphicsLayer = rememberGraphicsLayer() + val coroutineScope = rememberCoroutineScope() + // 抠图加载中状态 + val isLoading = remember { mutableStateOf(false) } + + val widthParam = navController.previousBackStackEntry?.savedStateHandle?.get("width") ?: 23f + val heightParam = navController.previousBackStackEntry?.savedStateHandle?.get("height") ?: 35f + + //显示服装和发型 + val viewClothing = remember { mutableStateOf(ViewType.VISIBLE) } + val viewHairstyle = remember { mutableStateOf(ViewType.VISIBLE) } + + // 人物图片选择和抠图相关状态 + val selectedImageUri = remember { mutableStateOf(null) } + val cutoutResultBitmap = remember { mutableStateOf(null) } + + // 头像编辑状态 + val headTransform = remember { mutableStateOf(TransformState()) } + val clothesTransform = remember { mutableStateOf(TransformState()) } + val hairTransform = remember { mutableStateOf(TransformState()) } + + var selectedTarget by remember { mutableStateOf(null) } + + // 辅助函数:判断点击位置 + fun hitTest(tap: Offset, state: TransformState, bmp: Bitmap?, canvasCenter: Offset): Boolean { + if (bmp == null) return false + val w = bmp.width * state.scale + val h = bmp.height * state.scale + val left = canvasCenter.x + state.offset.x - w / 2 + val top = canvasCenter.y + state.offset.y - h / 2 + return tap.x in left..(left + w) && tap.y in top..(top + h) + } + + // 服装和发型资源 + val selectedClothingBitmap = remember { mutableStateOf(null) } + val selectedHairstyleBitmap = remember { mutableStateOf(null) } + + // 当前选中的外观 + val selectedAppearance = remember { mutableStateOf(AppearanceType.BACKGROUND) } + // 当前选中的颜色索引 + val selectedColorIndex = remember { mutableIntStateOf(0) } + // 当前选中的衣服 + val selectedClothing = remember { mutableStateOf(clothingForFemales.getOrNull(0)) } + // 当前选中的发型 + val selectedHairstyle = remember { mutableStateOf(hairstyleForFemales.getOrNull(0)) } + // 当前选中的尺寸 + val selectedSize = remember { mutableStateOf(resizes.first { it.width == widthParam && it.height == heightParam }) } + + + // 图片选择启动器 + val imagePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri -> + uri?.let { + selectedImageUri.value = it + isLoading.value = true + + // 执行抠图操作 + Thread { + try { + val originalBitmap = ImageUtils.getBitmapFromUri(context, it) + originalBitmap?.let { bitmap -> + PhotoCutter.cutPureHead(bitmap) { croppedBitmap -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.mainExecutor.execute { + cutoutResultBitmap.value = croppedBitmap + isLoading.value = false + + // 重置头像变换 + headTransform.value = TransformState() + } + } + } + } + } catch (e: Exception) { + Log.e("CutoutScreen", "抠图失败: ${e.message}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.mainExecutor.execute { + isLoading.value = false + Toast.makeText(context, "抠图失败,请重试", Toast.LENGTH_SHORT).show() + } + } + } + }.start() + } + } + + // 加载服装资源 + fun loadClothingResource(clothing: ClothingBean?) { + if(clothing?.clothing == null){ + selectedClothingBitmap.value = null + }else{ + Thread { + val bitmap = ResourceManager.loadClothingResource(context, clothing.clothing) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.mainExecutor.execute { + selectedClothingBitmap.value = bitmap + } + } + }.start() + } + } + + // 加载发型资源 + fun loadHairstyleResource(hairstyle: HairstyleBean?) { + if(hairstyle?.hairstyle == null){ + selectedHairstyleBitmap.value = null + }else{ + Thread { + val bitmap = ResourceManager.loadHairstyleResource(context, hairstyle.hairstyle) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.mainExecutor.execute { + selectedHairstyleBitmap.value = bitmap + } + } + }.start() + } + } + + Scaffold { + LaunchedEffect(Unit) { + imagePickerLauncher.launch("image/*") + } + Column( + modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4)) + ) { + TitleBar(navController = navController, paddingValues = it, title = "", showSave = true){ + // 保存图片 + coroutineScope.launch { + // 从 Layer 捕获 Bitmap + val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap() + // 保存图片到系统相册(图片已按比例裁剪) + saveCanvasToGallery(context, bitmap, ExportFormat.JPG){fileName, isSuccess -> + if(isSuccess){ + Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show() + }else{ + Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show() + } + } + + /* + // 保存图片到系统相册(指定尺寸,如果targetWidth与targetHeight比原始值小太多会导致图片模糊) + saveCanvasToGallery(context, bitmap, ExportFormat.JPG, selectedSize.value.width.toInt(), selectedSize.value.height.toInt()){fileName, isSuccess -> + if(isSuccess){ + Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show() + }else{ + Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show() + } + } + */ + } + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + // 图片显示区域 - 支持头像编辑 + Box( + modifier = Modifier + .fillMaxWidth() + .padding(start = 38.dp, end = 38.dp + ) + .aspectRatio(selectedSize.value.width/selectedSize.value.height) + .background(Color(0xFFFFFFFF)) + .border(1.dp, Color(0xFFD8D8D8)) + ) { + if (isLoading.value) { + // 加载中状态 + Column( + modifier = Modifier.align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_loading), + contentDescription = "加载效果图片", + modifier = Modifier.size(48.dp) + ) + Text( + text = "正在抠图中...", + color = Color(0xFF767676), + fontSize = 12.sp, + modifier = Modifier.padding(top = 8.dp) + ) + } + } else if (cutoutResultBitmap.value != null) { + // 显示合成结果 + Canvas( + modifier = Modifier + .fillMaxSize() + .drawWithContent { + // 将内容绘制到 graphicsLayer 中 + graphicsLayer.record { + this@drawWithContent.drawContent() + } + drawLayer(graphicsLayer) + } + .clipToBounds() + .pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { + val down = awaitFirstDown() + val center = size.toSize().center + + // 1. 修改判定逻辑:只有当服装可见时,才进行它的 hitTest + selectedTarget = when { + // 处理发型 + viewHairstyle.value == ViewType.VISIBLE && hitTest(down.position, hairTransform.value, selectedHairstyleBitmap.value, center) -> "HAIR" + + // 处理服装 + viewClothing.value == ViewType.VISIBLE && hitTest(down.position, clothesTransform.value, selectedClothingBitmap.value, center) -> "CLOTHES" + + hitTest(down.position, headTransform.value, cutoutResultBitmap.value, center) -> "HEAD" + else -> null + } + } + } + } + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, rotation -> + selectedTarget?.let { target -> + // 更新逻辑保持不变... + val stateRef = when(target) { + "HEAD" -> headTransform + "CLOTHES" -> clothesTransform + else -> hairTransform + } + val bmp = when(target) { + "HEAD" -> cutoutResultBitmap.value + "CLOTHES" -> selectedClothingBitmap.value + else -> selectedHairstyleBitmap.value + } + + bmp?.let { b -> + val current = stateRef.value + val newScale = (current.scale * zoom).coerceIn(0.1f, 5f) + stateRef.value = current.copy( + offset = current.offset + pan, + scale = newScale, + rotation = if (target == "HEAD") current.rotation + rotation else current.rotation + ) + } + } + } + } + ) { + val canvasCenter = center + + // 1. 绘制背景 + drawRect(colors[selectedColorIndex.intValue]) + + // 2. 绘制头像 + cutoutResultBitmap.value?.let { bmp -> + withTransform({ + translate(headTransform.value.offset.x, headTransform.value.offset.y) + rotate(headTransform.value.rotation, canvasCenter) + scale(headTransform.value.scale, headTransform.value.scale, canvasCenter) + }) { + drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2)) + } + } + + // 3. 绘制服装 (根据状态[viewClothing]显示) + if (viewClothing.value == ViewType.VISIBLE) { + selectedClothingBitmap.value?.let { bmp -> + withTransform({ + translate(clothesTransform.value.offset.x, clothesTransform.value.offset.y) + scale(clothesTransform.value.scale, clothesTransform.value.scale, canvasCenter) + }) { + drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2)) + } + } + } + + // 4. 绘制发型 (根据状态[viewHairstyle]显示) + if (viewHairstyle.value == ViewType.VISIBLE) { + selectedHairstyleBitmap.value?.let { bmp -> + withTransform({ + translate(hairTransform.value.offset.x, hairTransform.value.offset.y) + scale(hairTransform.value.scale, hairTransform.value.scale, canvasCenter) + }) { + drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2)) + } + } + } + } + + + } else if (selectedImageUri.value != null) { + // 显示选中的原始图片 + AsyncImage( + model = selectedImageUri.value, + contentDescription = "选中的图片", + modifier = Modifier.fillMaxSize(), + contentScale = androidx.compose.ui.layout.ContentScale.Fit + ) + } else { + // 空状态 + Column( + modifier = Modifier.align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_image_empty_pld), + contentDescription = "截图", + ) + Text( + text = "点击选择图片", + color = Color(0xFFD8D8D8), + fontSize = 12.sp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } + } + + // 底部控制区域 + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + if (selectedAppearance.value == AppearanceType.CLOTHING || selectedAppearance.value == AppearanceType.HAIRSTYLE) { + // 服装和发型查看 + Row( + modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 16.dp) + ) { + ViewOption( + viewType = viewClothing.value, + title = "服装", + onClick = { + viewClothing.value = if (viewClothing.value == ViewType.HIDE) ViewType.VISIBLE else ViewType.HIDE + } + ) + Box(modifier = Modifier.wrapContentSize().padding(end = 8.dp)) + ViewOption( + viewType = viewHairstyle.value, + title = "发型", + onClick = { + viewHairstyle.value = if (viewHairstyle.value == ViewType.HIDE) ViewType.VISIBLE else ViewType.HIDE + } + ) + } + } else { + Row( + modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 16.dp) + ) {} + } + + /* + // 说明 + Row( + modifier = Modifier.wrapContentSize().padding(end = 16.dp).align(Alignment.Bottom) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_info), + contentDescription = "说明", + modifier = Modifier + .size(14.dp) + .align(Alignment.CenterVertically) + .clip(CircleShape) + .background(Color(0xFFF4F4F4)) + ) + Text( + text = "使用说明", + color = Color(0xFF767676), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.align(Alignment.Bottom).padding(start = 4.dp) + ) + } + */ + } + + // 底部画板 + DrawingBoardPicker( + // 颜色 + colors = colors, + // 服装 + clothingForMans = clothingForMans, + clothingForFemales = clothingForFemales, + // 发型 + hairstyleForMans = hairstyleForMans, + hairstyleForFemales = hairstyleForFemales, + // 尺寸 + sizes = resizes, + // 选中 + selectedAppearance = selectedAppearance, + selectedColorIndex = selectedColorIndex, + selectedClothing = selectedClothing, + selectedHairstyle = selectedHairstyle, + selectedSize = selectedSize, + // 新增回调函数 + onClothingSelected = { clothing -> + loadClothingResource(clothing) + }, + onHairstyleSelected = { hairstyle -> + loadHairstyleResource(hairstyle) + } + ) + } + } + } + } +} + +// 保存合成结果 +private fun saveCompositeResult(context: Context, compositeBitmap: Bitmap): Uri? { + try { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val imageFileName = "COMPOSITE_${timeStamp}.png" + + val storageDir = context.getExternalFilesDir(android.os.Environment.DIRECTORY_PICTURES) + val imageFile = File(storageDir, imageFileName) + + val outputStream = FileOutputStream(imageFile) + compositeBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + outputStream.flush() + outputStream.close() + + // 更新媒体库 + val mediaScanIntent = android.content.Intent(android.content.Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) + val uri = Uri.fromFile(imageFile) + mediaScanIntent.data = uri + context.sendBroadcast(mediaScanIntent) + + return uri + } catch (e: Exception) { + Log.e("CutoutScreen", "保存合成结果失败: ${e.message}") + return null + } +} + +@Composable +private fun ViewOption( + viewType: ViewType, + title: String, + onClick: () -> Unit, +){ + Box( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .background( + Color(0xFFFFFFFF), + shape = RoundedCornerShape(359.dp), + ) + .border( + width = 1.dp, + color = if(viewType == ViewType.VISIBLE) Color(0xFF000000) else Color(0xFFEDEDED), + shape = RoundedCornerShape(359.dp) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() + } + ) { + Row( + modifier = Modifier + .align(Alignment.Center) + .padding(start = 5.dp, end = 7.dp) + ) { + Image( + painter = painterResource(id = if(viewType == ViewType.VISIBLE) R.mipmap.ic_view else R.mipmap.ic_hide), + contentDescription = "图标", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterVertically) + ) + Text( + title, + color = if(viewType == ViewType.VISIBLE) Color(0xFF1A1A1A) else Color(0xFF767676), + fontSize = 14.sp, + lineHeight = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 2.dp) + ) + } + } +} + + +enum class ViewType { + HIDE, + VISIBLE +} + +@Preview(showBackground = true) +@Composable +private fun PreviewCutoutScreen() { + CutoutScreen(navController = rememberNavController()) +} diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/FormatScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/FormatScreen.kt new file mode 100644 index 0000000..c0414d0 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/FormatScreen.kt @@ -0,0 +1,176 @@ +package com.img.rabbit.pages.screen.make + +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +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.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import coil3.compose.AsyncImage +import com.img.rabbit.R +import com.img.rabbit.components.DrawingBoardFormatPicker +import com.img.rabbit.config.CommonData.formats +import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.utils.ExportFormat +import com.img.rabbit.utils.ImageUtils.convertToGallery +import com.img.rabbit.utils.ImageUtils.getBitmapFromUri +import kotlinx.coroutines.launch + +@Composable +fun FormatScreen(navController: NavController) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + // 当前选中的格式 + val selectedFormat = remember { mutableStateOf(formats.getOrNull(0)) } + + // 人物图片选择相关状态 + val selectedImageUri = remember { mutableStateOf(null) } + + // 图片选择启动器 + val imagePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri -> + uri?.let { + selectedImageUri.value = it + } + } + + Scaffold{ + LaunchedEffect(Unit) { + imagePickerLauncher.launch("image/*") + } + Column( + modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4)) + ) { + TitleBar(navController = navController, paddingValues = it, title = "", showSave = true) { + val bitmap = selectedImageUri.value?.let {uri-> + getBitmapFromUri(context, uri) + } + if(bitmap != null){ + // 保存图片 + coroutineScope.launch { + // 转换图片格式 + val format = when(selectedFormat.value?.title){ + ExportFormat.PNG.name -> ExportFormat.PNG + ExportFormat.GIF.name -> ExportFormat.GIF + ExportFormat.SVG.name -> ExportFormat.SVG + else -> ExportFormat.JPG + } + convertToGallery( + context = context, + sourceBitmap = bitmap, + format = format, + onSubmitResult = { fileName, isSuccess -> + if (isSuccess) { + Toast.makeText(context, "已保存至 $fileName", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show() + } + } + ) + } + } + + } + + Box( + modifier = Modifier + .fillMaxSize() + ){ + Box( + modifier = Modifier + .fillMaxWidth() + .padding(start = 38.dp, end = 38.dp, top = 14.dp) + .aspectRatio(300f / 420f) + .background(Color(0xFFFFFFFF)) + .border(1.dp, Color(0xFFD8D8D8)) + ){ + if (selectedImageUri.value != null) { + // 显示选中的原始图片 + AsyncImage( + model = selectedImageUri.value, + contentDescription = "选中的图片", + modifier = Modifier.fillMaxSize(), + contentScale = androidx.compose.ui.layout.ContentScale.Fit + ) + } else { + // 空状态 + Column( + modifier = Modifier.align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_image_empty_pld), + contentDescription = "空图", + ) + Text( + text = "去相册选一张美照吧~", + color = Color(0xFFD8D8D8), + fontSize = 12.sp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 9.dp) + .background(Color(0xFFAAAAAA), shape = RoundedCornerShape(7.dp)) + .align(Alignment.CenterHorizontally) + ){ + + Text( + text = "请选择图片导出格式,用于适配各种平台", + color = Color(0xFFFFFFFF), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.align(Alignment.Center) + ) + } + + Box(Modifier.height(14.dp)) + + //底部画板 + DrawingBoardFormatPicker( + //格式列表数据 + formats = formats, + //选中 + selectedFormat = selectedFormat, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt new file mode 100644 index 0000000..4f3ca39 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt @@ -0,0 +1,251 @@ +package com.img.rabbit.pages.screen.make + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.navigation.NavController +import com.img.rabbit.bean.LongImageBean +import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.utils.ExportFormat +import com.img.rabbit.utils.ImageUtils.getBitmapFromUri +import com.img.rabbit.utils.ImageUtils.saveLongToGallery +import github.leavesczy.matisse.CoilImageEngine +import github.leavesczy.matisse.Matisse +import github.leavesczy.matisse.MatisseContract +import github.leavesczy.matisse.MediaResource +import github.leavesczy.matisse.MediaType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +import androidx.compose.ui.graphics.asAndroidBitmap +import io.moyuru.cropify.Cropify +import io.moyuru.cropify.CropifyOption +import io.moyuru.cropify.rememberCropifyState + +@Composable +fun LongImageScreen(navController: NavController) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var imageItems by remember { mutableStateOf>(emptyList()) } + var editingIndex by remember { mutableStateOf(null) } + + val coroutineScope = rememberCoroutineScope() + val mediaPickerLauncher = + rememberLauncherForActivityResult(contract = MatisseContract()) { result: List? -> + if (!result.isNullOrEmpty()) { + scope.launch(Dispatchers.IO) { + val newItems = result.mapNotNull { mediaResource -> + val bitmap = getBitmapFromUri(context, mediaResource.uri) + bitmap?.let { + LongImageBean(uri = mediaResource.uri, bitmap = it) + } + } + + withContext(Dispatchers.Main) { + imageItems = imageItems + newItems + } + } + + } + } + + Scaffold { + LaunchedEffect(Unit) { + val matisse = Matisse( + maxSelectable = 10, + imageEngine = CoilImageEngine(), + mediaType = MediaType.ImageOnly + ) + mediaPickerLauncher.launch(matisse) + } + Column ( + modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4)) + ) { + TitleBar( + navController = navController, + paddingValues = it, + title = "拼长图", + showSave = true + ){ + coroutineScope.launch { + saveLongToGallery(context = context, items = imageItems, format = ExportFormat.JPG){ fileName, isSuccess -> + if (isSuccess) { + Toast.makeText(context, "已保存至 $fileName", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show() + } + } + } + } + + // 2. 预览列表(支持每张图独立缩放) + LazyColumn( + modifier = Modifier.weight(1f).padding(start = 30.dp, end = 30.dp, bottom = 30.dp) + ) { + itemsIndexed( + items = imageItems, + // 使用 URI 和 Bitmap 的 hashCode 组合作为 Key + key = { index, item -> "${item.uri}_${item.bitmap.hashCode()}" } + ) { index, item -> + Box(modifier = Modifier + .fillMaxWidth() + .clipToBounds() + .clickable { editingIndex = index } + ) { + Image( + bitmap = item.bitmap.asImageBitmap(), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ) + } + } + } + editingIndex?.let { index -> + FullScreenCropDialog( + item = imageItems[index], + onDismiss = { editingIndex = null }, + onConfirmed = { croppedBitmap -> + val newList = imageItems.toMutableList() + newList[index] = imageItems[index].copy(bitmap = croppedBitmap) + imageItems = newList + editingIndex = null + } + ) + } + + } + + } +} + + +@Composable +fun FullScreenCropDialog( + item: LongImageBean, + onDismiss: () -> Unit, + onConfirmed: (Bitmap) -> Unit +) { + val cropifyState = rememberCropifyState() + val cropifyOption = remember { + CropifyOption( + frameColor = Color.Cyan, + gridColor = Color.White.copy(alpha = 0.5f), + ) + } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) { + Box(modifier = Modifier.fillMaxSize()) { + Cropify( + bitmap = item.bitmap.asImageBitmap(), + state = cropifyState, + option = cropifyOption, // 传入配置项 + modifier = Modifier.fillMaxSize().padding(bottom = 100.dp), + onImageCropped = { croppedImageBitmap -> + onConfirmed(croppedImageBitmap.asAndroidBitmap()) + } + ) + + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 40.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + TextButton(onClick = onDismiss) { + Text("取消", color = Color.White) + } + Button( + onClick = { + // 4. 触发裁剪动作 + cropifyState.crop() + } + ) { + Text("确定裁剪") + } + } + } + } + } +} + + + + +@Preview(showBackground = true) +@Composable +private fun PreviewLongImageScreen(){ + LongImageScreen(navController = NavController(LocalContext.current)) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/ResizeScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/ResizeScreen.kt new file mode 100644 index 0000000..fbc14c6 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/ResizeScreen.kt @@ -0,0 +1,316 @@ +package com.img.rabbit.pages.screen.make + +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.toSize +import androidx.navigation.NavController +import com.img.rabbit.R +import com.img.rabbit.components.DrawingBoardCertificatePicker +import com.img.rabbit.config.CommonData.resizes +import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.utils.ExportFormat +import com.img.rabbit.utils.ImageUtils +import com.img.rabbit.utils.ImageUtils.saveCanvasToGallery +import kotlinx.coroutines.launch + +@Composable +fun ResizeScreen(navController: NavController) { + val context = LocalContext.current + val dealTarget = "RESIZE" + // 图片显示区域 - 支持头像编辑(用于最终数据的导出保存Bitmap) + val graphicsLayer = rememberGraphicsLayer() + val coroutineScope = rememberCoroutineScope() + //加载图片参数 + val widthParam = navController.previousBackStackEntry?.savedStateHandle?.get("width") ?: 23f + val heightParam = navController.previousBackStackEntry?.savedStateHandle?.get("height") ?: 35f + + + // 当前选中的尺寸 + val selectedSize = remember { mutableStateOf(resizes.first { it.width == widthParam && it.height == heightParam }) } + + // 加载中状态 + val isLoading = remember { mutableStateOf(false) } + // 人物图片选择相关状态 + val selectedImageUri = remember { mutableStateOf(null) } + val resizeBitmap = remember { mutableStateOf(null) } + + + var selectedTarget by remember { mutableStateOf(null) } + // 编辑状态 + val transform = remember { mutableStateOf(TransformState()) } + + // 图片选择启动器 + val imagePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri -> + uri?.let { + selectedImageUri.value = it + isLoading.value = true + + // 执行抠图操作 + Thread { + try { + val originalBitmap = ImageUtils.getBitmapFromUri(context, it) + originalBitmap?.let { bitmap -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.mainExecutor.execute { + resizeBitmap.value = bitmap + isLoading.value = false + + // 重置头像变换 + transform.value = TransformState() + } + } + + } + + } catch (e: Exception) { + Log.e("ResizeScreen", "加载图片失败: ${e.message}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.mainExecutor.execute { + isLoading.value = false + Toast.makeText(context, "加载失败,请重试", Toast.LENGTH_SHORT).show() + } + } + } + }.start() + } + } + + // 辅助函数:判断点击位置 + fun hitTest(tap: Offset, state: TransformState, bmp: Bitmap?, canvasCenter: Offset): Boolean { + if (bmp == null) return false + val w = bmp.width * state.scale + val h = bmp.height * state.scale + val left = canvasCenter.x + state.offset.x - w / 2 + val top = canvasCenter.y + state.offset.y - h / 2 + return tap.x in left..(left + w) && tap.y in top..(top + h) + } + + Scaffold{ + LaunchedEffect(Unit) { + imagePickerLauncher.launch("image/*") + } + + Column( + modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4)) + ) { + TitleBar(navController = navController, paddingValues = it, title = "", showSave = true){ + // 保存图片 + coroutineScope.launch { + // 从 Layer 捕获 Bitmap + val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap() + // 保存图片到系统相册(图片已按比例裁剪) + saveCanvasToGallery(context, bitmap, ExportFormat.JPG){fileName, isSuccess -> + if(isSuccess){ + Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show() + }else{ + Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show() + } + } + + /* + // 保存图片到系统相册(指定尺寸,如果targetWidth与targetHeight比原始值小太多会导致图片模糊) + saveCanvasToGallery(context, bitmap, ExportFormat.JPG, selectedSize.value.width.toInt(), selectedSize.value.height.toInt()){fileName, isSuccess -> + if(isSuccess){ + Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show() + }else{ + Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show() + } + } + */ + } + } + + Box( + modifier = Modifier + .fillMaxSize() + ){ + Box( + modifier = Modifier + .fillMaxWidth() + .padding(start = 38.dp, end = 38.dp, top = 14.dp) + .aspectRatio(selectedSize.value.width/selectedSize.value.height) + .background(Color(0xFFFFFFFF)) + .border(1.dp, Color(0xFFD8D8D8)) + ){ + + if (isLoading.value) { + // 加载中状态 + Column( + modifier = Modifier.align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_loading), + contentDescription = "加载效果图片", + modifier = Modifier.size(48.dp) + ) + Text( + text = "加载中...", + color = Color(0xFF767676), + fontSize = 12.sp, + modifier = Modifier.padding(top = 8.dp) + ) + } + } else if (resizeBitmap.value != null) { + // 显示合成结果(原始图片) + Canvas( + modifier = Modifier + .fillMaxSize() + .drawWithContent { + // 将内容绘制到 graphicsLayer 中 + graphicsLayer.record { + this@drawWithContent.drawContent() + } + drawLayer(graphicsLayer) + } + .clipToBounds() + .pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { + val down = awaitFirstDown() + val center = size.toSize().center + + selectedTarget = when { + hitTest(down.position, transform.value, resizeBitmap.value, center) -> dealTarget + else -> null + } + } + } + } + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, rotation -> + selectedTarget?.let { target -> + // 更新逻辑保持不变... + val stateRef = transform + + val bmp = resizeBitmap.value + + bmp?.let { b -> + val current = stateRef.value + val newScale = (current.scale * zoom).coerceIn(0.1f, 5f) + stateRef.value = current.copy( + offset = current.offset + pan, + scale = newScale, + rotation = if (target == dealTarget) current.rotation + rotation else current.rotation + ) + } + } + } + } + ) { + val canvasCenter = center + resizeBitmap.value?.let { bmp -> + withTransform({ + translate(transform.value.offset.x, transform.value.offset.y) + rotate(transform.value.rotation, canvasCenter) + scale(transform.value.scale, transform.value.scale, canvasCenter) + }) { + drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2)) + } + } + } + + } else { + // 空状态 + Column( + modifier = Modifier.align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_image_empty_pld), + contentDescription = "截图", + ) + Text( + text = "去相册选一张美照吧~", + color = Color(0xFFD8D8D8), + fontSize = 12.sp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 9.dp) + .background(Color(0xFFAAAAAA), shape = RoundedCornerShape(7.dp)) + .align(Alignment.CenterHorizontally) + ){ + + Text( + text = "将已有证件照尺寸进行修改,用于适配各种场景", + color = Color(0xFFFFFFFF), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.align(Alignment.Center) + ) + } + + Box(Modifier.height(14.dp)) + + //底部画板 + DrawingBoardCertificatePicker( + //尺寸列表数据 + sizes = resizes, + //选中 + selectedSize = selectedSize, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/FeedbackScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/FeedbackScreen.kt new file mode 100644 index 0000000..1dbe057 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/FeedbackScreen.kt @@ -0,0 +1,419 @@ +package com.img.rabbit.pages.screen.mine + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.components.ImagePicker +import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.viewmodel.FeedbackViewModel + +@Composable +fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewModel = viewModel()) { + Scaffold{ + Column( + modifier = Modifier.fillMaxSize() + ) { + TitleBar(navController = navController, paddingValues = it, title = "意见反馈") + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(start = 16.dp, end = 16.dp) + ){ + // 意见反馈内容 + Column( + modifier = Modifier.fillMaxSize() + ) { + // 意见反馈类型 + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ) { + Text( + text = "*", + color = Color(0xFFEA0000), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + ) + Text( + text = "您想反馈的功能类型", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.wrapContentSize() + ) + } + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 12.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(34.dp) + .weight(1f) + .background( + Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FUNCTION) 0xFF252525 else 0xFFFFFFFF), + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + Color(0xFFD8D8D8), + shape = RoundedCornerShape(8.dp) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { viewModel.setFeedbackType(FeedbackViewModel.FeedbackType.FUNCTION) } + ){ + Text( + text = "功能问题", + color = Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FUNCTION) 0xFFC2FF43 else 0xFF1A1A1A), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentSize() + .align(Alignment.Center) + ) + } + Box( + modifier = Modifier.width(21.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(34.dp) + .weight(1f) + .background( + Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FEATURE) 0xFF252525 else 0xFFFFFFFF), + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + Color(0xFFD8D8D8), + shape = RoundedCornerShape(8.dp) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { viewModel.setFeedbackType(FeedbackViewModel.FeedbackType.FEATURE) } + ){ + Text( + text = "优化建议", + color = Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FEATURE) 0xFFC2FF43 else 0xFF1A1A1A), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentSize() + .align(Alignment.Center) + ) + } + Box( + modifier = Modifier.width(21.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(34.dp) + .weight(1f) + .background( + Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.OTHER) 0xFF252525 else 0xFFFFFFFF), + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + Color(0xFFD8D8D8), + shape = RoundedCornerShape(8.dp) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { viewModel.setFeedbackType(FeedbackViewModel.FeedbackType.OTHER) } + ){ + Text( + text = "其他", + color = Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.OTHER) 0xFFC2FF43 else 0xFF1A1A1A), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentSize() + .align(Alignment.Center) + ) + } + } + //补充反馈内容 + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ) { + Text( + text = "*", + color = Color(0xFFEA0000), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + ) + Text( + text = "请补充详细问题或意见", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.wrapContentSize() + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(150.dp) + .padding(top = 12.dp) + .background( + Color(0xFFEEEEEE), + shape = RoundedCornerShape(12.dp) + ) + .border( + width = 1.dp, + Color(0xFFEEEEEE), + shape = RoundedCornerShape(12.dp) + ) + ){ + BasicTextField( + value = viewModel.feedbackMore, + onValueChange = {content-> + if (content.length <= 200) { + viewModel.setFeedbackMore(content) + } + }, + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .padding(start = 18.dp, top = 7.dp, end = 18.dp, bottom = 7.dp), + textStyle = androidx.compose.ui.text.TextStyle( + color = Color.Black, + fontSize = 14.sp + ), + singleLine = false, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + keyboardType = androidx.compose.ui.text.input.KeyboardType.Text, + imeAction = androidx.compose.ui.text.input.ImeAction.Done + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + ) { + if (viewModel.feedbackMore.isEmpty()) { + Text( + "请在这里输入内容", + color = Color(0xFF767676), + fontSize = 14.sp + ) + } + innerTextField() + } + } + ) + Text( + text = "${viewModel.feedbackMore.length}/200", + color = Color(0xFF767676), + fontSize = 14.sp, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 18.dp, bottom = 7.dp) + ) + } + } + //提供相关图片 + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ) { + Text( + text = "请提供相关问题的图片", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + ) + Text( + text = "(选填,最多可上传三张)", + color = Color(0xFFAAAAAA), + fontSize = 11.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.wrapContentSize() + .align(Alignment.CenterVertically) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight().padding(top = 12.dp) + ) { + ImagePicker( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + imageHeight = 100.dp, + aspectRatio = 100f / 100f, + maxCount = 3, + addButtonName = "添加图片", + currentImageUris = viewModel.currentImageUris, + currentImagePaths = emptyList(), // 新增参数 + onImagesUpdated = { uris, _ -> + viewModel.setCurrentImageUris(uris) + } + ) + } + } + //联系方式 + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ) { + Text( + text = "联系方式", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + ) + Text( + text = "(选填,可以留下您的手机号、微信、邮箱)", + color = Color(0xFFAAAAAA), + fontSize = 11.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.wrapContentSize() + .align(Alignment.CenterVertically) + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 12.dp) + .background( + Color(0xFFEEEEEE), + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + Color(0xFFEEEEEE), + shape = RoundedCornerShape(8.dp) + ) + ){ + BasicTextField( + value = viewModel.feedbackContact, + onValueChange = {content-> + if (content.length <= 50) { + viewModel.setFeedbackContact(content) + } + }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(Color.Transparent) + .padding(start = 18.dp, top = 10.dp, end = 18.dp, bottom = 10.dp), + textStyle = androidx.compose.ui.text.TextStyle( + color = Color.Black, + fontSize = 14.sp + ), + singleLine = true, + maxLines = 1, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + keyboardType = androidx.compose.ui.text.input.KeyboardType.Text, + imeAction = androidx.compose.ui.text.input.ImeAction.Done + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(Color.Transparent) + ) { + if (viewModel.feedbackContact.isEmpty()) { + Text( + "请输入您的联系方式", + color = Color(0xFF767676), + fontSize = 14.sp + ) + } + innerTextField() + } + } + ) + } + } + } + + + //提交 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp, bottom = 30.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 提交反馈 + viewModel.submitFeedback() + } + .align(Alignment.BottomCenter) + ) { + Text( + "提交", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewFeedbackScreen(){ + FeedbackScreen(navController = rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/OnlineServiceScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/OnlineServiceScreen.kt new file mode 100644 index 0000000..6d66a1d --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/OnlineServiceScreen.kt @@ -0,0 +1,78 @@ +package com.img.rabbit.pages.screen.mine + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R + +@Composable +fun OnlineServiceScreen(navController: NavHostController) { + Scaffold{ + Column( + modifier = Modifier.fillMaxSize().padding(it) + ) { + // 顶部栏 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 返回按钮 + Icon( + painter = painterResource(id = R.mipmap.ic_back), + contentDescription = "返回", + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { navController.popBackStack() } + .padding(end = 16.dp) + ) + Column( + modifier = Modifier.fillMaxWidth().padding(end = 26.dp) + ) { + + Text( + text = "", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } + Box( + modifier = Modifier.fillMaxWidth().padding(end = 26.dp) + ){ + //TODO 在线客服内容 + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewOnlineServiceScreen(){ + OnlineServiceScreen(navController = rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/SettingScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/SettingScreen.kt new file mode 100644 index 0000000..1e39332 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/SettingScreen.kt @@ -0,0 +1,320 @@ +package com.img.rabbit.pages.screen.mine + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R +import com.img.rabbit.pages.toolbar.TitleBar + +@Composable +fun SettingScreen(navController: NavHostController) { + Scaffold{ + Column( + modifier = Modifier.fillMaxSize() + ) { + TitleBar(navController = navController, paddingValues = it, title = "设置") + + Box( + modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp) + ){ + //TODO 设置内容 + Column( + modifier = Modifier + .wrapContentSize() + .padding(top = 16.dp) + .background( + Color(0xFFFFFFFF), + RoundedCornerShape(18.dp) + ) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = Color(0x4DE5E5E5), + spotColor = Color(0x4DE5E5E5) + ) + ){ + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 跳转页面 + //navController.navigate("feedback") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "清除缓存", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Row( + modifier = Modifier.wrapContentSize() + ) { + Text( + "12MB", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterVertically), + color = Color(0xFF767676), + textAlign = TextAlign.Center + ) + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 跳转绑定页面 + navController.navigate("bindAccount") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "账号绑定", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 跳转账号管理页面 + navController.navigate("managerAccount") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "账号管理", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 跳转关于我们页面 + navController.navigate("aboutMine") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "关于我们", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + } + + //切换/退出账号 + Column( + modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter) + ) { + //账号切换 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp, bottom = 18.dp) + .background( + Color(0x00000000), + shape = RoundedCornerShape(359.dp), + ) + .border(width = 1.dp, color = Color(0xFF000000), shape = RoundedCornerShape(359.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 账号切换 + navController.navigate("managerAccount") + } + ) { + Text( + "账号切换", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + + //退出登录 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp, bottom = 30.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 退出登录 + } + ) { + Text( + "退出登录", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewSettingScreen(){ + SettingScreen(navController = rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/VersionUpdateScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/VersionUpdateScreen.kt new file mode 100644 index 0000000..67d3d82 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/VersionUpdateScreen.kt @@ -0,0 +1,16 @@ +package com.img.rabbit.pages.screen.mine + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController + +@Composable +fun VersionUpdateScreen(navController: NavHostController) { +} + +@Preview(showBackground = true) +@Composable +private fun PreviewVersionUpdateScreen(){ + VersionUpdateScreen(navController = rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt new file mode 100644 index 0000000..5546738 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt @@ -0,0 +1,186 @@ +package com.img.rabbit.pages.screen.mine.setting + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R +import com.img.rabbit.config.Common.agreementUrl +import com.img.rabbit.config.Common.privacyUrl +import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.utils.UrlLinkUtils.openAgreement + +@Composable +fun AboutScreen(navController: NavHostController) { + Scaffold{ + val context = LocalContext.current + Column( + modifier = Modifier.fillMaxSize() + ) { + TitleBar(navController = navController, paddingValues = it, title = "关于我们") + + Box( + modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp) + ){ + //TODO 设置内容 + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 100.dp) + ){ + Image( + painter = painterResource(id = R.mipmap.ic_launcher_logo), + contentDescription = "关于我们", + modifier = Modifier + .width(100.dp) + .aspectRatio(1f) + .align(Alignment.CenterHorizontally) + ) + + + + Text( + text = "截图兔", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 24.dp) + ) + + + Column( + modifier = Modifier + .wrapContentSize() + .padding(top = 24.dp) + .background( + Color(0xFFFFFFFF), + RoundedCornerShape(18.dp) + ) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = Color(0x4DE5E5E5), + spotColor = Color(0x4DE5E5E5) + ) + ){ + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 跳转用户协议页面 + openAgreement(context, agreementUrl) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "用户协议", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 跳转隐私政策页面 + openAgreement(context, privacyUrl) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "隐私政策", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewAboutScreen(){ + AboutScreen(navController = rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountBindScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountBindScreen.kt new file mode 100644 index 0000000..800ec23 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountBindScreen.kt @@ -0,0 +1,380 @@ +package com.img.rabbit.pages.screen.mine.setting + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R +import com.img.rabbit.pages.toolbar.TitleBar + +@SuppressLint("UnrememberedMutableState") +@Composable +fun AccountBindScreen(navController: NavHostController) { + val showDialogStatus = mutableStateOf(false) + Scaffold{ + Box( + modifier = Modifier.fillMaxSize() + ){ + Column( + modifier = Modifier.fillMaxSize() + ) { + TitleBar(navController = navController, paddingValues = it, title = "账号绑定") + + Box( + modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp) + ){ + //TODO 设置内容 + Column( + modifier = Modifier + .wrapContentSize() + .padding(top = 16.dp) + .background( + Color(0xFFFFFFFF), + RoundedCornerShape(18.dp) + ) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = Color(0x4DE5E5E5), + spotColor = Color(0x4DE5E5E5) + ) + ){ + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 跳转绑定与解绑手机号 + showDialogStatus.value = !showDialogStatus.value + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "手机号", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Row( + modifier = Modifier.wrapContentSize() + ) { + Text( + "+86 123****123", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterVertically), + color = Color(0xFF767676), + textAlign = TextAlign.Center + ) + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + } + Box( + modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background( + Color(0x4DBBBBBB) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 跳转绑定或解绑微信 + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.weight(1f) + ){ + Text( + text = "微信账号", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + + + Row( + modifier = Modifier.wrapContentSize() + ) { + Text( + "去绑定", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterVertically), + color = Color(0xFF767676), + textAlign = TextAlign.Center + ) + Image( + painter = painterResource(id = R.mipmap.ic_arrow_right), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .wrapContentSize() + .padding(end = 12.dp) + .align(Alignment.CenterVertically) + ) + } + } + } + } + } + + if(showDialogStatus.value){ + UnBindPhoneDialog( + showDialogStatus = showDialogStatus, + onStatusChange = { + showDialogStatus.value = it + } + ) + } + } + } +} + +@Composable +private fun UnBindPhoneDialog( + showDialogStatus: MutableState, + onStatusChange: (Boolean) -> Unit +){ + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0x80000000)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + showDialogStatus.value = false + }, + verticalArrangement = Arrangement.Center + ){ + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 36.dp) + .background(Color.White, shape = RoundedCornerShape(26.dp)) + .align(Alignment.CenterHorizontally) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + //什么都不用做,只是解决点击穿透问题 + } + ) { + Image( + painter = painterResource(id = R.mipmap.ic_dialog_top_mask1), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 46.dp) + ) { + Text( + text = "解开绑定的手机号?", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterHorizontally) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(14.dp) + ) + + Text( + text = "当前绑定的手机号码为", + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF767676), + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterHorizontally) + ) + + Text( + text = "+86 123****456", + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF767676), + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterHorizontally) + ) + + + Box( + modifier = Modifier + .fillMaxWidth() + .height(16.dp) + ) + + //切换/退出账号 + Row( + modifier = Modifier.fillMaxWidth().padding(start = 18.dp, end = 18.dp, bottom = 20.dp) + ) { + //取消解绑手机号 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .weight(1f) + .background( + Color(0x00000000), + shape = RoundedCornerShape(359.dp), + ) + .border(width = 1.dp, color = Color(0xFF000000), shape = RoundedCornerShape(359.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 取消解绑手机号 + showDialogStatus.value = false + } + ) { + Text( + "取消", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + + Box( + modifier = Modifier + .width(11.dp) + ) + + + + //退出登录 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .weight(1f) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 确定解绑手机号 + showDialogStatus.value = false + } + ) { + Text( + "确定", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + } + } + + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewAccountBindScreen(){ + AccountBindScreen(navController = rememberNavController()) +} + +@SuppressLint("UnrememberedMutableState") +@Preview(showBackground = true) +@Composable +private fun PreviewUnBindPhoneDialog(){ + UnBindPhoneDialog(showDialogStatus = mutableStateOf(true), onStatusChange = {}) +} diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt new file mode 100644 index 0000000..63db93b --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt @@ -0,0 +1,270 @@ +package com.img.rabbit.pages.screen.mine.setting + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R +import com.img.rabbit.bean.UserInfo +import com.img.rabbit.pages.toolbar.TitleBar + +@Composable +fun AccountManagerScreen(navController: NavHostController) { + val userList by remember { + mutableStateOf( + listOf( + UserInfo(1, "张三", true), + UserInfo(2, "李四", false), + UserInfo(3, "王五", false), + ) + ) + } + Scaffold{ + Box( + modifier = Modifier.fillMaxSize() + ){ + Image( + painter = painterResource(id = R.mipmap.ic_account_switch_top_mask), + contentDescription = "蒙层", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + + + Column( + modifier = Modifier.fillMaxSize() + ) { + TitleBar(navController = navController, paddingValues = it, title = "") + + Column( + modifier = Modifier.fillMaxWidth().padding(top = 100.dp) + ) { + + Text( + text = "轻触头像以切换帐号", + color = Color(0xFF3D3D3D), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + + Box( + modifier = Modifier.fillMaxSize().padding(top = 55.dp, start = 17.dp, end = 17.dp) + ){ + //TODO 设置内容 + LazyColumn { + // 添加五个项目 + items(userList.size) { index -> + ListItem(userList[index]) + } + item { + AddItem(navController) + } + } + } + } + } + } +} + +@Composable +private fun ListItem(item: UserInfo){ + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = 10.dp) + .background( + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(8.dp) + ) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = Color(0x4DE5E5E5), + spotColor = Color(0x4DE5E5E5) + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 11.dp, vertical = 17.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 切换账号 + //navController.navigate("feedback") + }, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.mipmap.ic_launcher_logo), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(46.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(8.dp)) + ) + + Column( + modifier = Modifier.weight(1f) + ){ + Text( + text = "小林", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + Text( + text = "ID:123456", + fontSize = 12.sp, + color = Color(0xFF767676), + fontWeight = FontWeight.Normal, + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + + Row( + modifier = Modifier.wrapContentSize() + ) { + Checkbox( + checked = item.login, + onCheckedChange = { isChecked -> + //viewModel.setIsPolicyAgreement(isChecked) + }, + modifier = Modifier + .size(16.dp) + .scale(0.35f) + .padding(start = 6.dp) + .background( + if (item.login) Color(0xFF252525) + else Color.Transparent, + shape = RoundedCornerShape(36.dp) + ) + .border( + width = 1.dp, + color = if (item.login) Color(0xFF252525) + else Color(0xFFCCCCCC), + shape = RoundedCornerShape(36.dp) + ) + .align(Alignment.CenterVertically), + colors = androidx.compose.material3.CheckboxDefaults.colors( + checkedColor = Color.Transparent, // 隐藏默认背景 + uncheckedColor = Color.Transparent, // 隐藏默认背景 + checkmarkColor = Color.White + ) + + ) + } + } + } +} + + +@Composable +private fun AddItem(navController: NavController){ + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = 16.dp) + .background( + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(8.dp) + ) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = Color(0x4DE5E5E5), + spotColor = Color(0x4DE5E5E5) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + //TODO 添加账号 + navController.navigate("login") + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 11.dp, vertical = 17.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.mipmap.ic_picture_add), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(46.dp) + .align(Alignment.CenterVertically) + .background(Color(0xFFF3F3F3), shape = RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + ) + + Column( + modifier = Modifier.weight(1f) + ){ + Text( + text = "添加账号", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewAccountManagerScreen(){ + AccountManagerScreen(navController = rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/screen/other/CameraGuideScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/other/CameraGuideScreen.kt new file mode 100644 index 0000000..80a0ec0 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/screen/other/CameraGuideScreen.kt @@ -0,0 +1,220 @@ +package com.img.rabbit.pages.screen.other + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.img.rabbit.R +import com.img.rabbit.pages.toolbar.TitleBar + +@Composable +fun CameraGuideScreen(navController: NavController) { + Scaffold { + Box( + modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4)) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.End + ) { + Image( + painter = painterResource(id = R.mipmap.ic_camera_mask), + contentDescription = null, + modifier = Modifier.wrapContentSize(), + ) + } + TitleBar(navController = navController, paddingValues = it, title = "", showSave = false) + + Column( + modifier = Modifier.fillMaxSize().padding(top = 104.dp) + ){ + Text( + text = "如何拍照?", + color = Color(0xFF1A1A1A), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 16.dp) + ) + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp, top = 20.dp) + .background(Color(0xFFFFFFFF), + shape = RoundedCornerShape(16.dp) + ) + ) { + Row ( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp, bottom = 20.dp) + ){ + Text( + text = "1.注意光线均匀\n 2.不能耸肩或斜肩\n 3.正对镜头,双耳露出\n 4.不要佩戴眼镜\n 5.注意纯色墙作背景\n 6.避免衣服与背景色相同", + color = Color(0xFF1A1A1A), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(top = 20.dp) + ) + Column( + modifier = Modifier + .fillMaxWidth().padding(top = 11.dp), + horizontalAlignment = Alignment.End + ) { + Image( + painter = painterResource(id = R.mipmap.ic_photo_sample), + contentDescription = null, + modifier = Modifier.wrapContentSize().align(Alignment.End) + ) + } + } + } + + Text( + text = "证件照背景颜色要求", + color = Color(0xFF1A1A1A), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 24.dp, start = 16.dp) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp, top = 20.dp) + .background(Color(0xFFFFFFFF), + shape = RoundedCornerShape(16.dp) + ) + ) { + Row ( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp, top = 18.dp) + ){ + Box( + modifier = Modifier + .size(18.dp) + .align(Alignment.CenterVertically) + .background(Color(0xFFFFFFFF), shape = RoundedCornerShape(56.dp)) + .border(0.5.dp, Color(0xFFAAAAAA), shape = RoundedCornerShape(56.dp)) + ) + Text( + text = "白色背景:", + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + lineHeight = 12.sp, + modifier = Modifier.padding(start = 4.dp).align(Alignment.CenterVertically) + ) + } + Text( + text = "用于护照、签证、驾驶证、身份证、二代身份证、驾驶证、黑白证件、医保卡、港澳通行证、护照等。", + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + lineHeight = 12.sp, + modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp) + ) + + Row ( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp, top = 24.dp) + ){ + Box( + modifier = Modifier + .size(18.dp) + .align(Alignment.CenterVertically) + .background(Color(0xFF438EDB), shape = RoundedCornerShape(56.dp)) + .border(0.5.dp, Color(0xFFAAAAAA), shape = RoundedCornerShape(56.dp)) + ) + Text( + text = "蓝色背景:", + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + lineHeight = 12.sp, + modifier = Modifier.padding(start = 4.dp).align(Alignment.CenterVertically) + ) + } + Text( + text = "用于毕业证、工作证、简历等 (蓝色数值为R:0 G:191 B:243 或 C:67 M:Z Y:0 k:0)", + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + lineHeight = 12.sp, + modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp) + ) + + Row ( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp, top = 24.dp) + ){ + Box( + modifier = Modifier + .size(18.dp) + .align(Alignment.CenterVertically) + .background(Color(0xFFFF0000), shape = RoundedCornerShape(56.dp)) + .border(0.5.dp, Color(0xFFAAAAAA), shape = RoundedCornerShape(56.dp)) + ) + Text( + text = "红色背景:", + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + lineHeight = 12.sp, + modifier = Modifier.padding(start = 4.dp).align(Alignment.CenterVertically) + ) + } + Text( + text = "用于保险、医保、IC卡、暂住证、结婚照 (红色数值为R:255 G:0 B:0 或 C:0 M:99 Y:100 K: 0)", + color = Color(0xFF1A1A1A), + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + lineHeight = 12.sp, + modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp) + ) + + Box(modifier = Modifier + .fillMaxWidth() + .height(16.dp)) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewCameraGuideScreen() { + CameraGuideScreen(navController = NavController(LocalContext.current)) +} diff --git a/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt new file mode 100644 index 0000000..d3ff909 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt @@ -0,0 +1,111 @@ +package com.img.rabbit.pages.toolbar + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.img.rabbit.R + +@Composable +fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, onSubmit: (() -> Unit)? = null) { + Box( + modifier = Modifier.fillMaxWidth().padding(paddingValues) + ){ + Row( + modifier = Modifier + .fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 返回按钮 + Icon( + painter = painterResource(id = R.mipmap.ic_back), + contentDescription = "返回", + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { navController?.popBackStack() } + .padding(end = 26.dp) + ) + Column( + modifier = Modifier.fillMaxWidth().weight(1f) + ) { + + Text( + text = title ?: "", + color = Color(0xFF1A1A1A), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + + if(showSave) { + Box( + modifier = Modifier + .width(60.dp) + .height(36.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 保存 + onSubmit?.invoke() + } + ) { + Text( + "保存", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.Center) + ) + } + }else{ + Box( + modifier = Modifier + .width(60.dp) + .height(36.dp) + ) + } + + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewTitleBar() { + TitleBar(navController = rememberNavController(), paddingValues = PaddingValues(), title = "截图兔", showSave = true) +} diff --git a/app/src/main/java/com/img/rabbit/route/ScreenRoute.kt b/app/src/main/java/com/img/rabbit/route/ScreenRoute.kt new file mode 100644 index 0000000..abd4421 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/route/ScreenRoute.kt @@ -0,0 +1,29 @@ +package com.img.rabbit.route + +// 定义导航路由 +sealed class ScreenRoute(val route: String) { + //Tab页面 + object Home : ScreenRoute("home") + object Mine : ScreenRoute("mine") + //首页(抠图) + object Cutout : ScreenRoute("cutout") + //首页(证件) + object Resize : ScreenRoute("resize") + //首页(格式) + object Format : ScreenRoute("format") + //首页(拍照指南) + object CameraGuide : ScreenRoute("cameraGuide") + //首页(长图) + object LongImage : ScreenRoute("longImage") + + //我的页面(Mine) + object Feedback : ScreenRoute("feedback") + object OnlineService : ScreenRoute("onlineService") + object Login : ScreenRoute("login") + object Setting : ScreenRoute("setting") + + //设置页面(Setting) + object BindAccount : ScreenRoute("bindAccount") + object ManagerAccount : ScreenRoute("managerAccount") + object AboutMine : ScreenRoute("aboutMine") +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/AgreementTextHelper.kt b/app/src/main/java/com/img/rabbit/utils/AgreementTextHelper.kt new file mode 100644 index 0000000..5c1c5e7 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/AgreementTextHelper.kt @@ -0,0 +1,62 @@ +package com.img.rabbit.utils + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableString +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.View +import android.widget.TextView +import androidx.core.graphics.toColorInt +import androidx.core.net.toUri + +object AgreementTextHelper { + + fun setupAgreementTextView(fullText: String, targets: Map, textView: TextView, isUnderlineText: Boolean = true, onAgreementClick: (String) -> Unit) { + val spannableString = SpannableString(fullText) + + // 设置各个协议的点击区域 + targets.forEach { (targetKey, target) -> + setClickableSpan(spannable = spannableString, target = target, isUnderlineText = isUnderlineText, color = "#FF767676".toColorInt()) { + onAgreementClick(targetKey) + } + } + + textView.text = spannableString + textView.movementMethod = LinkMovementMethod.getInstance() + textView.highlightColor = Color.TRANSPARENT + + // 设置长按不显示复制菜单 + textView.setOnLongClickListener { true } + } + + private fun setClickableSpan( + spannable: Spannable, + target: String, + isUnderlineText: Boolean, + color: Int, + onClick: () -> Unit + ) { + val start = spannable.indexOf(target) + if (start >= 0) { + val end = start + target.length + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + onClick() + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.color = color + ds.isUnderlineText = isUnderlineText + ds.typeface = Typeface.DEFAULT_BOLD + } + } + spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/AppDataStore.kt b/app/src/main/java/com/img/rabbit/utils/AppDataStore.kt new file mode 100644 index 0000000..fcbb01d --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/AppDataStore.kt @@ -0,0 +1,48 @@ +package com.img.rabbit.utils + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +// 创建DataStore实例 +val Context.dataStore: DataStore by preferencesDataStore(name = "app_settings") + +// DataStore键定义 +object AppPreferencesKeys { + val NAVIGATION_BAR_VISIBLE = booleanPreferencesKey("navigation_bar_visible") + val USER_LOGGED_IN = booleanPreferencesKey("user_logged_in") +} + +class AppDataStore(private val context: Context) { + + // 获取NavigationBar显示状态 + val isNavigationBarVisible: Flow = context.dataStore.data + .map { preferences -> + preferences[AppPreferencesKeys.NAVIGATION_BAR_VISIBLE] ?: true + } + + // 设置NavigationBar显示状态 + suspend fun setNavigationBarVisible(visible: Boolean) { + context.dataStore.edit { preferences -> + preferences[AppPreferencesKeys.NAVIGATION_BAR_VISIBLE] = visible + } + } + + // 获取用户登录状态 + val isUserLoggedIn: Flow = context.dataStore.data + .map { preferences -> + preferences[AppPreferencesKeys.USER_LOGGED_IN] ?: false + } + + // 设置用户登录状态 + suspend fun setUserLoggedIn(loggedIn: Boolean) { + context.dataStore.edit { preferences -> + preferences[AppPreferencesKeys.USER_LOGGED_IN] = loggedIn + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java new file mode 100644 index 0000000..c339e10 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java @@ -0,0 +1,128 @@ +package com.img.rabbit.utils; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.widget.Toast; + +public class Bitmap2SVG +{ + public static boolean convert(Context context, File output, Bitmap input){ + Bitmap2SVG svg; + try + { + svg = new Bitmap2SVG( new PrintWriter( new BufferedWriter( new FileWriter( output ) ) ), true ); + } catch (IOException e) + { + return false; + } + + svg.printSVGHeader( input.getWidth(), input.getHeight() ); + svg.printSVGBody( input ); + svg.printSVGFooter(); + + saveToGallery(context, output, "displayName.svg"); + + return true; + } + + private boolean trc_del; + private PrintWriter pw; + private int w, h; + + private Bitmap2SVG( PrintWriter pw, boolean trc_delete ) + { + this.pw = pw; + trc_del = trc_delete; + } + private void printSVGHeader( int width, int height ) + { + pw.println( "" ); + + pw.print( "" ); + } + private void printSVGFooter() + { + pw.println( "" ); + pw.close(); + } + private void printSVGBody( Bitmap bmp ) + { + int x, y, c, a; + w = bmp.getWidth(); + h = bmp.getHeight(); + for ( y = 0 ; y < h ; ++y ) + { + for ( x = 0 ; x < w ;++x ) + { + c = bmp.getPixel( x, y ); + a = Color.alpha( c ); + if ( trc_del && a == 0 ){ continue; } + printDot( x, y, rgb( c ), a ); + } + } + } + private void printDot( int x, int y, String rgb, int a ) + { + pw.print( "" ); + } + private static String rgb( int c ) + { + return String.format( "%02x%02x%02x", Color.red( c ), Color.green( c ), Color.blue( c ) ); + } + + public static void saveToGallery(Context context, File svgFile, String displayName) { + ContentValues values = new ContentValues(); + // 关键点:显式指定 MIME 类型为 SVG + values.put(MediaStore.Images.Media.MIME_TYPE, "image/svg+xml"); + values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName + ".svg"); + values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES); + + ContentResolver resolver = context.getContentResolver(); + Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + + if (uri != null) { + try (OutputStream os = resolver.openOutputStream(uri); + FileInputStream fis = new FileInputStream(svgFile)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = fis.read(buffer)) != -1) { + os.write(buffer, 0, len); + } + // 此时文件已带上正确的 MIME 类型存入相册 + Toast.makeText(context, "SVG已保存", Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/app/src/main/java/com/img/rabbit/utils/CoilEngine.kt b/app/src/main/java/com/img/rabbit/utils/CoilEngine.kt new file mode 100644 index 0000000..41b9faf --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/CoilEngine.kt @@ -0,0 +1,61 @@ +package com.img.rabbit.utils + +import android.content.Context +import android.widget.ImageView +import coil3.load +import coil3.request.crossfade +import coil3.request.transformations +import coil3.transform.RoundedCornersTransformation +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class CoilEngine : ImageEngine { + + // 加载普通图片 + override fun loadImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + imageView.load(url) + } + + override fun loadImage( + context: Context?, + imageView: ImageView?, + url: String?, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + imageView?.load(url) + } + + // 加载相册目录封面 + override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + imageView.load(url) { + transformations(RoundedCornersTransformation(8f)) + size(180, 180) // 优化内存,封面图没必要太大 + } + } + + // 加载图片列表中的小图 + override fun loadGridImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + imageView.load(url) { + size(200, 200) + crossfade(true) + } + } + + // 暂停/恢复加载(Coil 自动处理,可留空) + override fun pauseRequests(context: Context?) {} + override fun resumeRequests(context: Context?) {} + + companion object { + private var instance: CoilEngine? = null + fun createCoilEngine(): CoilEngine { + return instance ?: synchronized(this) { + instance ?: CoilEngine().also { instance = it } + } + } + } +} diff --git a/app/src/main/java/com/img/rabbit/utils/ImagePickerUtils.kt b/app/src/main/java/com/img/rabbit/utils/ImagePickerUtils.kt new file mode 100644 index 0000000..a897672 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/ImagePickerUtils.kt @@ -0,0 +1,134 @@ +package com.img.rabbit.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.MediaStore +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * 图片选择工具类 + */ +object ImagePickerUtils { + + /** + * 创建相机拍照的Intent + */ + fun createCameraIntent(context: Context): Pair { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val storageDir = context.externalCacheDir ?: context.cacheDir + val imageFile = File.createTempFile( + "JPEG_${timeStamp}_", + ".jpg", + storageDir + ) + + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { + putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(imageFile)) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + + return Pair(intent, imageFile) + } + + /** + * 创建相册选择的Intent + */ + fun createGalleryIntent(): Intent { + return Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply { + type = "image/*" + } + } + + /** + * 获取图片的Uri路径 + */ + fun getImageUri(context: Context, file: File): Uri { + return androidx.core.content.FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + file + ) + } +} + +/** + * 图片选择结果数据类 + */ +data class ImagePickerResult( + val uri: Uri? = null, + val filePath: String? = null, + val error: String? = null +) + +/** + * 可组合函数:创建相机启动器 + */ +@Composable +fun rememberCameraLauncher( + context: Context = LocalContext.current, + onResult: (ImagePickerResult) -> Unit +) = rememberLauncherForActivityResult( + ActivityResultContracts.TakePicture() +) { success -> + if (success) { + // 这里需要与createCameraIntent配合使用,实际使用时需要保存临时文件路径 + onResult(ImagePickerResult(error = "需要配合createCameraIntent使用")) + } else { + onResult(ImagePickerResult(error = "拍照取消或失败")) + } +} + +/** + * 可组合函数:创建相册启动器(单个图片选择) + */ +@Composable +fun rememberGalleryLauncher( + onResult: (ImagePickerResult) -> Unit +) = rememberLauncherForActivityResult( + ActivityResultContracts.PickVisualMedia() +) { uri -> + if (uri != null) { + onResult(ImagePickerResult(uri = uri)) + } else { + onResult(ImagePickerResult(error = "未选择图片")) + } +} + +/** + * 可组合函数:创建相册启动器(多个图片选择) + */ +@Composable +fun rememberMultipleGalleryLauncher( + onResult: (List) -> Unit +) = rememberLauncherForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia() +) { uris -> + val results = uris.map { uri -> + ImagePickerResult(uri = uri) + } + onResult(results) +} + +/** + * 可组合函数:创建通用图片选择启动器(支持相机和相册) + */ +@Composable +fun rememberImagePickerLauncher( + onResult: (ImagePickerResult) -> Unit +) = rememberLauncherForActivityResult( + ActivityResultContracts.GetContent() +) { uri -> + if (uri != null) { + onResult(ImagePickerResult(uri = uri)) + } else { + onResult(ImagePickerResult(error = "未选择图片")) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt new file mode 100644 index 0000000..6b9c403 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt @@ -0,0 +1,562 @@ +package com.img.rabbit.utils + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import kotlin.apply + +import android.graphics.* +import androidx.core.graphics.createBitmap +import com.img.rabbit.bean.LongImageBean +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import androidx.core.graphics.withClip + +object ImageUtils { + fun decodeSampledBitmapFromResource( + context: Context, + resId: Int, + reqWidth: Int, + reqHeight: Int + ): Bitmap { + // 首先设置inJustDecodeBounds=true来获取图片尺寸 + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeResource(context.resources, resId, options) + + // 计算inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) + Log.d("ImageUtils", "Sample size: ${options.inSampleSize}") + + // 根据计算的inSampleSize来解码图片 + options.inJustDecodeBounds = false + return BitmapFactory.decodeResource(context.resources, resId, options) + } + + // 新增:从byte数组加载图片的方法 + fun decodeSampledBitmapFromByteArray( + byteArray: ByteArray, + reqWidth: Int, + reqHeight: Int + ): Bitmap { + // 首先设置inJustDecodeBounds=true来获取图片尺寸 + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options) + + // 计算inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) + + // 根据计算的inSampleSize来解码图片 + options.inJustDecodeBounds = false + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options) + } + + private fun calculateInSampleSize( + options: BitmapFactory.Options, + reqWidth: Int, + reqHeight: Int + ): Int { + // 原始图片的尺寸 + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + val halfHeight = height / 2 + val halfWidth = width / 2 + + // 计算最大的inSampleSize值,该值为2的幂,并保持 + // 高度和宽度均大于请求的高度和宽度 + while ((halfHeight / inSampleSize) >= reqHeight + && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2 + } + } + return inSampleSize + } + + /** + * 将Bitmap保存到设备存储 + * @param bitmap 要保存的Bitmap对象 + * @param context 上下文 + * @param format 图片格式(Bitmap.CompressFormat.JPEG 或 Bitmap.CompressFormat.PNG) + * @param quality 压缩质量(0-100) + * @return 保存的文件路径,如果保存失败则返回null + */ + fun saveBitmapToStorage( + bitmap: Bitmap, + context: Context, + format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG, + quality: Int = 90 + ): String? { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val imageFileName = "JPEG_${timeStamp}_" + + return try { + val imageFile: File + val imagePath: String? + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Android 10及以上使用MediaStore + val contentValues = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, "$imageFileName.${format.name.lowercase()}") + put(MediaStore.Images.Media.MIME_TYPE, "image/${format.name.lowercase()}") + put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + "DoctorApp") + } + + val resolver = context.contentResolver + val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + imageUri?.let { uri -> + resolver.openOutputStream(uri)?.use { outputStream -> + bitmap.compress(format, quality, outputStream) + } + return uri.toString() + } + } else { + // Android 9及以下使用传统文件存储 + val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES + File.separator + "DoctorApp") + storageDir?.let { dir -> + if (!dir.exists() && !dir.mkdirs()) { + Log.e("ImageUtils", "无法创建存储目录") + return null + } + + imageFile = File(dir, "$imageFileName.${format.name.lowercase()}") + val outputStream = FileOutputStream(imageFile) + bitmap.compress(format, quality, outputStream) + outputStream.flush() + outputStream.close() + + // 将图片添加到媒体库 + MediaStore.Images.Media.insertImage(context.contentResolver, imageFile.absolutePath, imageFile.name, null) + + // 通知媒体扫描器扫描文件 + context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(imageFile))) + + return imageFile.absolutePath + } + } + null + } catch (e: IOException) { + Log.e("ImageUtils", "保存图片失败", e) + null + } + } + + /** + * 将字节数组直接保存为图片文件 + * @param byteArray 图片字节数组 + * @param context 上下文 + * @param format 图片格式 + * @return 保存的文件路径,如果保存失败则返回null + */ + fun saveByteArrayToStorage( + byteArray: ByteArray, + context: Context, + format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG + ): String? { + return try { + val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + saveBitmapToStorage(bitmap, context, format) + } catch (e: Exception) { + Log.e("ImageUtils", "保存字节数组为图片失败", e) + null + } + } + + /** + * 从URI获取Bitmap + * @param context 上下文 + * @param uri 图片URI + * @return Bitmap对象,如果获取失败则返回null + */ + fun getBitmapFromUri(context: Context, uri: Uri): Bitmap? { + return try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + BitmapFactory.decodeStream(inputStream) + } + } catch (e: Exception) { + Log.e("ImageUtils", "从URI获取Bitmap失败", e) + null + } + } + + /** + * 将URI对应的图片保存到存储 + * @param context 上下文 + * @param uri 图片URI + * @param format 图片格式 + * @param quality 压缩质量 + * @return 保存的文件路径,如果保存失败则返回null + */ + fun saveUriToStorage( + context: Context, + uri: Uri, + format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG, + quality: Int = 90 + ): String? { + return try { + val bitmap = getBitmapFromUri(context, uri) + bitmap?.let { + saveBitmapToStorage(it, context, format, quality) + } + } catch (e: Exception) { + Log.e("ImageUtils", "保存URI图片失败", e) + null + } + } + + + /** + * 保存长图到相册 + * @param context 上下文 + * @param items 长图数据列表 + * @param format 导出格式 + * @param onSubmitResult 回调函数,用于通知保存结果 + */ + fun saveLongToGallery( + context: Context, + items: List, + format: ExportFormat, + onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit + ) { + if (items.isEmpty()) return + + // 1. 定义长图的统一输出宽度 + val canvasWidth = 1080f + + // 2. 计算每张图按比例缩放后的总高度 + var totalHeight = 0f + val renderConfigs = items.map { item -> + // 计算当前图片填满 1080 宽度所需的缩放比例 + val scale = canvasWidth / item.bitmap.width + // 缩放后的实际物理高度 + val scaledHeight = item.bitmap.height * scale + totalHeight += scaledHeight + scale to scaledHeight + } + + // 3. 创建大图画布 + val resultBitmap = Bitmap.createBitmap(canvasWidth.toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888) + val canvas = Canvas(resultBitmap) + val paint = Paint(Paint.ANTI_ALIAS_FLAG) // 开启抗锯齿,防止缩放模糊 + var currentY = 0f + + // 4. 依次绘制每一张图片 + items.forEachIndexed { index, item -> + val (scale, scaledHeight) = renderConfigs[index] + + val matrix = Matrix() + // 先按比例缩放 + matrix.postScale(scale, scale) + // 再平移到当前 Y 轴位置(实现无缝衔接) + matrix.postTranslate(0f, currentY) + + canvas.drawBitmap(item.bitmap, matrix, paint) + + // 关键:累加【缩放后】的高度,确保下一张图紧贴上一张 + currentY += scaledHeight + } + + // 5. 调用你原有的保存逻辑 + saveCanvasToGallery(context, resultBitmap, format, onSubmitResult) + } + + + /** + * 保存位图到系统相册 + * @param context 上下文 + * @param bitmap 要保存的位图 + * @param format 导出格式(PNG/JPG) + * @param onSubmitResult 回调函数,参数为文件名和是否成功 + */ + fun saveCanvasToGallery( + context: Context, + bitmap: Bitmap, + format: ExportFormat, + onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit + ) { + val extension = if (format == ExportFormat.JPG) "jpg" else "png" + val mimeType = if (format == ExportFormat.JPG) "image/jpeg" else "image/png" + val compressFormat = if (format == ExportFormat.JPG) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG + + val filename = "MyRabbit_${System.currentTimeMillis()}.$extension" + + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put(MediaStore.MediaColumns.MIME_TYPE, mimeType) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera") + } + } + + val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + uri?.let { + context.contentResolver.openOutputStream(it)?.use { stream -> + // PNG 忽略 quality 参数(无损),JPG 使用 100 表示最高质量 + bitmap.compress(compressFormat, 100, stream) + onSubmitResult(filename, true) + } + } ?: run { + onSubmitResult(filename, false) + } + } + + /** + * 保存位图到系统相册(如果targetWidth与targetHeight比原始值小太多会导致图片模糊) + * @param context 上下文 + * @param bitmap 要保存的位图 + * @param format 导出格式(PNG/JPG) + * @param targetWidth 目标宽度 + * @param targetHeight 目标高度 + * @param onSubmitResult 回调函数,参数为文件名和是否成功 + */ + @SuppressLint("UseKtx") + fun saveCanvasToGallery( + context: Context, + bitmap: Bitmap, + format: ExportFormat, + targetWidth: Int, + targetHeight: Int, + onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit + ) { + // 1. 修复 Hardware Bitmap 报错并确保使用 ARGB_8888 高精度配置 + val softwareBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + bitmap.config == Bitmap.Config.HARDWARE) { + bitmap.copy(Bitmap.Config.ARGB_8888, false) + } else { + // 如果不是硬件位图,也建议检查并转换为高精度配置以防模糊 + bitmap + } + + val extension = if (format == ExportFormat.JPG) "jpg" else "png" + val mimeType = if (format == ExportFormat.JPG) "image/jpeg" else "image/png" + val compressFormat = if (format == ExportFormat.JPG) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG + + // 2. 创建目标尺寸位图 (显式指定 Config.ARGB_8888 保证清晰度) + val scaledResult = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(scaledResult) + + // 3. 处理背景色 (如果是 JPG 填充白色,PNG 保持透明) + if (format == ExportFormat.JPG) { + canvas.drawColor(Color.WHITE) + } + + // 4. 【核心算法:计算等比例缩放】 + val srcWidth = softwareBitmap.width.toFloat() + val srcHeight = softwareBitmap.height.toFloat() + + // 使用 float 计算比例,避免整数除法丢失精度导致模糊 + val scale = (targetWidth.toFloat() / srcWidth).coerceAtMost(targetHeight.toFloat() / srcHeight) + + val finalWidth = srcWidth * scale + val finalHeight = srcHeight * scale + val left = (targetWidth - finalWidth) / 2f + val top = (targetHeight - finalHeight) / 2f + + // 5. 【关键:高清绘制配置】 + val paint = Paint().apply { + isAntiAlias = true // 开启抗锯齿,边缘更平滑 + isFilterBitmap = true // 开启位图过滤,这是解决缩放模糊的最核心设置 + isDither = true // 开启抖动,色彩过渡更自然 + } + + val destRect = RectF(left, top, left + finalWidth, top + finalHeight) + // 绘图 + canvas.drawBitmap(softwareBitmap, null, destRect, paint) + + // 6. 保存到 MediaStore + val filename = "MyRabbit_${System.currentTimeMillis()}.$extension" + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put(MediaStore.MediaColumns.MIME_TYPE, mimeType) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera") + } + } + + val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + try { + uri?.let { + context.contentResolver.openOutputStream(it)?.use { stream -> + // 100 表示不压缩质量 + scaledResult.compress(compressFormat, 100, stream) + onSubmitResult(filename, true) + } + } ?: onSubmitResult(filename, false) + } catch (e: Exception) { + e.printStackTrace() + onSubmitResult(filename, false) + } finally { + // 7. 内存回收 + if (softwareBitmap != bitmap) softwareBitmap.recycle() + // 注意:如果后面还要用 scaledResult,不要在这里 recycle + // scaledResult.recycle() + } + } + + /** + * 转换为相册可保存的格式(PNG/JPG/SVG/GIF) + * @param context 上下文 + * @param sourceBitmap 原始位图 + * @param format 导出格式(PNG/JPG/SVG/GIF) + * @param onSubmitResult 回调函数,参数为文件名和是否成功 + */ + fun convertToGallery( + context: Context, + sourceBitmap: Bitmap, + format: ExportFormat, + onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit + ) { + // 1. 预处理:缩放并获得高清软件位图(防止 Hardware Bitmap 报错) + val softwareBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && sourceBitmap.config == Bitmap.Config.HARDWARE) { + sourceBitmap.copy(Bitmap.Config.ARGB_8888, false) + } else sourceBitmap + + val scaledBitmap = createScaledBitmap(softwareBitmap, sourceBitmap.width, sourceBitmap.height) + + // 2. 准备 MediaStore 容器 + val filename = "MyRabbit_Export_${System.currentTimeMillis()}.${format.extension}" + val contentValues = if (format == ExportFormat.SVG) { + ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put(MediaStore.MediaColumns.MIME_TYPE, "image/svg+xml") // 关键:SVG 的 MIME 类型 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera") + } + } + }else{ + ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put(MediaStore.MediaColumns.MIME_TYPE, format.mimeType) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera") + } + } + } + + val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + // 3. 根据格式执行不同的转换编码 + try { + uri?.let { + context.contentResolver.openOutputStream(it)?.use { stream -> + when (format) { + ExportFormat.JPG -> scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) + ExportFormat.PNG -> scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + ExportFormat.GIF -> encodeToGif(scaledBitmap, stream) + ExportFormat.SVG -> encodeToSvg(scaledBitmap, stream) + } + onSubmitResult(filename, true) + } + } ?: onSubmitResult(filename, false) + } catch (e: Exception) { + onSubmitResult(e.message ?: "Unknown Error", false) + } + } + + + /** + * 创建等比例适配的缩放位图 + * @param src 原始位图 + * @param destW 目标宽度 + * @param destH 目标高度 + * @return 缩放后的位图 + */ + private fun createScaledBitmap(src: Bitmap, destW: Int, destH: Int): Bitmap { + val result = createBitmap(destW, destH) + val canvas = Canvas(result) + val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG or Paint.DITHER_FLAG) + + // 等比例适配逻辑 + val scale = (destW.toFloat() / src.width).coerceAtMost(destH.toFloat() / src.height) + val left = (destW - src.width * scale) / 2f + val top = (destH - src.height * scale) / 2f + + canvas.drawBitmap(src, null, RectF(left, top, left + src.width * scale, top + src.height * scale), paint) + return result + } + + /** + * 具体的 GIF 编码逻辑 + * @param bitmap 原始位图 + * @param outputStream 输出流 + */ + private fun encodeToGif(bitmap: Bitmap, outputStream: OutputStream) { + // Android 原生不支持直接写 GIF,通常需使用外部库或简单的 Bitmap 压缩 + // 这里演示通过 Bitmap 压缩模拟格式导出逻辑 + try { + // 注意:Android 原生 compress 不支持 GIF 格式, + // 实际开发中若需生成多帧 GIF,建议使用 'jp.co.cyberagent.android:gpuimage' 或第三方编码器 + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + outputStream.flush() + outputStream.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * 具体的 SVG 编码逻辑 + * @param bitmap 原始位图 + * @param stream 输出流 + */ + private fun encodeToSvg(bitmap: Bitmap, stream: OutputStream) { + try { + val width = bitmap.width + val height = bitmap.height + + val bos = ByteArrayOutputStream() + // 使用 PNG 保证透明度 + bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos) + + // 必须使用 NO_WRAP 标志,如果不加 NO_WRAP,Base64 字符串每 76 个字符会插入一个换行符,导致 SVG 标签断开 + val base64Data = android.util.Base64.encodeToString(bos.toByteArray(), android.util.Base64.NO_WRAP) + + // XML 命名空间地址 + val svgContent = """ + + + + + """.trimIndent() + + stream.write(svgContent.toByteArray(Charsets.UTF_8)) + stream.flush() + } catch (e: Exception) { + Log.e("ImageUtils", "SVG 编码失败: ${e.message}") + throw e + } + } + +} + +enum class ExportFormat(val extension: String, val mimeType: String) { + JPG("jpg", "image/jpeg"), + PNG("png", "image/png"), + GIF("gif", "image/gif"), + SVG("svg", "image/svg+xml") +} + diff --git a/app/src/main/java/com/img/rabbit/utils/NetworkMonitor.kt b/app/src/main/java/com/img/rabbit/utils/NetworkMonitor.kt new file mode 100644 index 0000000..2ab111f --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/NetworkMonitor.kt @@ -0,0 +1,87 @@ +package com.img.rabbit.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +object NetworkMonitor { + private val _networkStatus = MutableLiveData() + val networkStatus: LiveData = _networkStatus + + private var connectivityManager: ConnectivityManager? = null + private var networkCallback: ConnectivityManager.NetworkCallback? = null + + @SuppressLint("ObsoleteSdkInt") + fun initialize(context: Context) { + connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // Android 7.0+ 使用registerDefaultNetworkCallback + networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + _networkStatus.postValue(true) + } + + override fun onLost(network: Network) { + _networkStatus.postValue(false) + } + + override fun onUnavailable() { + _networkStatus.postValue(false) + } + } + connectivityManager?.registerDefaultNetworkCallback(networkCallback as ConnectivityManager.NetworkCallback) + } else { + // Android 7.0以下使用传统方式 + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + + networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + _networkStatus.postValue(true) + } + + override fun onLost(network: Network) { + _networkStatus.postValue(false) + } + } + connectivityManager?.registerNetworkCallback(networkRequest, networkCallback as ConnectivityManager.NetworkCallback) + } + + // 初始化当前网络状态 + _networkStatus.value = isNetworkAvailable(context) + } + + @SuppressLint("ObsoleteSdkInt") + fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + capabilities != null && ( + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + ) + } else { + @Suppress("DEPRECATION") + val networkInfo = connectivityManager.activeNetworkInfo + @Suppress("DEPRECATION") + networkInfo != null && networkInfo.isConnected + } + } + + fun unregister() { + networkCallback?.let { + connectivityManager?.unregisterNetworkCallback(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/NetworkStatus.kt b/app/src/main/java/com/img/rabbit/utils/NetworkStatus.kt new file mode 100644 index 0000000..e7ca23a --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/NetworkStatus.kt @@ -0,0 +1,28 @@ +package com.img.rabbit.utils + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build + +object NetworkStatus { + + @SuppressLint("ObsoleteSdkInt") + fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + @Suppress("DEPRECATION") + val networkInfo = connectivityManager.activeNetworkInfo + networkInfo != null && networkInfo.isConnected + } + } + fun openNetworkSettings(context: Context) { + context.startActivity(android.content.Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/PermissionUtils.kt b/app/src/main/java/com/img/rabbit/utils/PermissionUtils.kt new file mode 100644 index 0000000..17aa9f9 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/PermissionUtils.kt @@ -0,0 +1,167 @@ +package com.img.rabbit.utils + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat + +/** + * 权限管理工具类 + */ +object PermissionUtils { + + // 相机权限 + const val CAMERA_PERMISSION = Manifest.permission.CAMERA + + // 存储权限(根据Android版本区分) + val STORAGE_PERMISSIONS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf(Manifest.permission.READ_MEDIA_IMAGES) + } else { + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + /** + * 检查权限是否已授予 + */ + fun hasPermissions(context: Context, vararg permissions: String): Boolean { + return permissions.all { permission -> + ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED + } + } + + /** + * 检查相机权限 + */ + fun hasCameraPermission(context: Context): Boolean { + return hasPermissions(context, CAMERA_PERMISSION) + } + + /** + * 检查存储权限 + */ + fun hasStoragePermission(context: Context): Boolean { + return hasPermissions(context, *STORAGE_PERMISSIONS) + } + + /** + * 检查所有图片相关权限 + */ + fun hasImagePermissions(context: Context): Boolean { + return hasCameraPermission(context) && hasStoragePermission(context) + } + + /** + * 检查相册权限(只需要存储权限) + */ + fun hasGalleryPermission(context: Context): Boolean { + return hasStoragePermission(context) + } + + /** + * 跳转到应用设置页面 + */ + fun openAppSettings(context: Context) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + if (context is Activity) { + context.startActivity(intent) + } else { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + } +} + +/** + * 权限状态数据类 + */ +data class PermissionState( + val hasCameraPermission: Boolean = false, + val hasStoragePermission: Boolean = false, + val shouldShowRationale: Boolean = false +) + +/** + * 可组合函数:管理权限请求 + */ +@Composable +fun rememberPermissionState(): MutableState { + val context = LocalContext.current + return remember { + mutableStateOf( + PermissionState( + hasCameraPermission = PermissionUtils.hasCameraPermission(context), + hasStoragePermission = PermissionUtils.hasStoragePermission(context) + ) + ) + } +} + +/** + * 可组合函数:创建权限请求启动器 + */ +@Composable +fun rememberPermissionLauncher( + context: Context = LocalContext.current, + onGranted: () -> Unit = {}, + onDenied: () -> Unit = {}, + onRationale: () -> Unit = {} +) = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() +) { permissions -> + val allGranted = permissions.values.all { it } + val shouldShowRationale = permissions.keys.any { permission -> + androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale( + context as Activity, + permission + ) + } + + if (allGranted) { + onGranted() + } else if (shouldShowRationale) { + onRationale() + } else { + onDenied() + } +} + +/** + * 可组合函数:创建相机权限启动器 + */ +@Composable +fun rememberCameraPermissionLauncher( + context: Context = LocalContext.current, + onGranted: () -> Unit = {}, + onDenied: () -> Unit = {}, + onRationale: () -> Unit = {} +) = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() +) { isGranted -> + if (isGranted) { + onGranted() + } else { + val shouldShowRationale = androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale( + context as Activity, + Manifest.permission.CAMERA + ) + if (shouldShowRationale) { + onRationale() + } else { + onDenied() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/PhotoCutter.kt b/app/src/main/java/com/img/rabbit/utils/PhotoCutter.kt new file mode 100644 index 0000000..ab5c89e --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/PhotoCutter.kt @@ -0,0 +1,129 @@ +package com.img.rabbit.utils + +import android.graphics.* +import com.google.mlkit.vision.common.InputImage +import com.google.mlkit.vision.segmentation.Segmentation +import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions +import androidx.core.graphics.createBitmap + +import android.graphics.* +import androidx.compose.ui.geometry.Offset +import com.google.mlkit.vision.face.FaceContour +import com.google.mlkit.vision.face.FaceDetection +import com.google.mlkit.vision.face.FaceDetectorOptions +import androidx.core.graphics.get + +// 变换状态模型 +data class TransformState( + val offset: Offset = Offset.Zero, + val scale: Float = 1f, + val rotation: Float = 0f +) + + + +object PhotoCutter { + + fun cutPureHead(sourceBitmap: Bitmap, callback: (Bitmap?) -> Unit) { + // 1. 配置分割器:单图模式 + val options = SelfieSegmenterOptions.Builder() + .setDetectorMode(SelfieSegmenterOptions.SINGLE_IMAGE_MODE) + .build() + val segmenter = Segmentation.getClient(options) + val image = InputImage.fromBitmap(sourceBitmap, 0) + + // 2. 开始处理 + segmenter.process(image) + .addOnSuccessListener { mask -> + val resultBitmap = generateTransparentBitmap(sourceBitmap, mask.buffer) + callback(resultBitmap) + } + .addOnFailureListener { + callback(null) + } + } + + private fun generateTransparentBitmap( + source: Bitmap, + maskBuffer: java.nio.ByteBuffer, + ): Bitmap { + val width = source.width + val height = source.height + val result = createBitmap(width, height) + + // 将掩码缓冲区重置 + maskBuffer.rewind() + + // 逐像素处理 + val pixels = IntArray(width * height) + source.getPixels(pixels, 0, width, 0, 0, width, height) + + for (i in 0 until width * height) { + // 获取当前像素的人像概率(0.0 ~ 1.0) + val confidence = maskBuffer.float + + // 如果置信度低于 0.5,认为该像素是背景,设为完全透明 + if (confidence < 0.5) { + pixels[i] = Color.TRANSPARENT + } + } + + result.setPixels(pixels, 0, width, 0, 0, width, height) + return result + } +} + + + +//object PhotoCutter { +// +// fun cutHeadFromImage(sourceBitmap: Bitmap, callback: (Bitmap?) -> Unit) { +// // 1. 配置分割器:单图模式 +// val options = SelfieSegmenterOptions.Builder() +// .setDetectorMode(SelfieSegmenterOptions.SINGLE_IMAGE_MODE) +// .build() +// val segmenter = Segmentation.getClient(options) +// val image = InputImage.fromBitmap(sourceBitmap, 0) +// +// // 2. 开始处理 +// segmenter.process(image) +// .addOnSuccessListener { mask -> +// val resultBitmap = generateTransparentBitmap(sourceBitmap, mask.buffer, mask.width, mask.height) +// callback(resultBitmap) +// } +// .addOnFailureListener { +// callback(null) +// } +// } +// +// private fun generateTransparentBitmap( +// source: Bitmap, +// maskBuffer: java.nio.ByteBuffer, +// maskWidth: Int, +// maskHeight: Int +// ): Bitmap { +// val width = source.width +// val height = source.height +// val result = createBitmap(width, height) +// +// // 将掩码缓冲区重置 +// maskBuffer.rewind() +// +// // 逐像素处理 +// val pixels = IntArray(width * height) +// source.getPixels(pixels, 0, width, 0, 0, width, height) +// +// for (i in 0 until width * height) { +// // 获取当前像素的人像概率(0.0 ~ 1.0) +// val confidence = maskBuffer.float +// +// // 如果置信度低于 0.5,认为该像素是背景,设为完全透明 +// if (confidence < 0.5) { +// pixels[i] = Color.TRANSPARENT +// } +// } +// +// result.setPixels(pixels, 0, width, 0, 0, width, height) +// return result +// } +//} diff --git a/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt new file mode 100644 index 0000000..ce376fa --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt @@ -0,0 +1,16 @@ +package com.img.rabbit.utils + +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri + +object UrlLinkUtils { + fun openAgreement(context: Context, url: String) { + // 打开服务协议 + Intent(Intent.ACTION_VIEW, url.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }.let { intent -> + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/FeedbackViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/FeedbackViewModel.kt new file mode 100644 index 0000000..f27c678 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/FeedbackViewModel.kt @@ -0,0 +1,60 @@ +package com.img.rabbit.viewmodel + +import android.net.Uri +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel + +class FeedbackViewModel : ViewModel() { + val isLoading = mutableStateOf(true) + + fun setLoading(loading: Boolean) { + isLoading.value = loading + } + + // 反馈类型 + private val _feedbackType = mutableStateOf(FeedbackType.FUNCTION) + val feedbackType: FeedbackType + get() = _feedbackType.value + + fun setFeedbackType(feedbackType: FeedbackType) { + _feedbackType.value = feedbackType + } + + // 补充反馈 + private val _feedbackMore = mutableStateOf("") + val feedbackMore: String + get() = _feedbackMore.value + + fun setFeedbackMore(feedbackMore: String) { + _feedbackMore.value = feedbackMore + } + + //提供图片 + private val _currentImageUris = mutableStateOf(emptyList()) + val currentImageUris: List + get() = _currentImageUris.value + + fun setCurrentImageUris(currentImageUris: List) { + _currentImageUris.value = currentImageUris + } + + // 联系方式 + private val _feedbackContact = mutableStateOf("") + val feedbackContact: String + get() = _feedbackContact.value + + fun setFeedbackContact(feedbackContact: String) { + _feedbackContact.value = feedbackContact + } + + // 提交反馈 + fun submitFeedback() { + setLoading(true) + } + + enum class FeedbackType(val keyName: String, val value: String) { + FUNCTION("function", "功能问题"), + FEATURE("feature", "功能建议"), + OTHER("other", "其他") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt new file mode 100644 index 0000000..2bd5445 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt @@ -0,0 +1,76 @@ +package com.img.rabbit.viewmodel + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.tencent.mmkv.MMKV + +@SuppressLint("ObsoleteSdkInt") +class GeneralViewModel(application: Application) : AndroidViewModel(application) { + val kv by lazy { MMKV.defaultMMKV() } + private val _networkStatus = MutableLiveData() + val networkStatus: LiveData = _networkStatus + fun setNetworkStatus(status: Boolean) { + _networkStatus.value = status + } + + private val _isNavigationBarVisible = MutableLiveData() + val isNavigationBarVisible: LiveData = _isNavigationBarVisible + + private val connectivityManager = + application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + _networkStatus.postValue(true) + } + + override fun onLost(network: Network) { + _networkStatus.postValue(false) + } + } + + init { + // 注册网络监听 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager.registerDefaultNetworkCallback(networkCallback) + } else { + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(request, networkCallback) + } + + // 初始化状态 + _networkStatus.value = isNetworkAvailable() + } + + private fun isNetworkAvailable(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + @Suppress("DEPRECATION") + val networkInfo = connectivityManager.activeNetworkInfo + networkInfo != null && networkInfo.isConnected + } + } + + fun setNavigationBarVisible(visible: Boolean){ + _isNavigationBarVisible.value = visible + } + + override fun onCleared() { + super.onCleared() + connectivityManager.unregisterNetworkCallback(networkCallback) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000..c42d8e9 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt @@ -0,0 +1,151 @@ +package com.img.rabbit.viewmodel + +import android.app.Activity +import android.util.Log +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.compose.runtime.State +import com.img.rabbit.pages.LoginScreenType +import com.g.gysdk.GYManager +import com.g.gysdk.GYResponse +import com.g.gysdk.GyCallBack +import com.g.gysdk.GyConfig +import com.img.rabbit.bean.OnekeyPreLogin +import kotlinx.serialization.json.Json + +class LoginViewModel : ViewModel() { + private val TAG = "LoginViewModel" + private val ONEKEY_TAG = "OneKeyLoginViewModel" + val loginScreenType = mutableStateOf(LoginScreenType.LOGIN_NORMAL) + // 登录用户名 + val userName = mutableStateOf("") + // 登录验证码 + val captcha = mutableStateOf("") + + // 是否同意政策协议 + private val _policyAgreement = mutableStateOf(false) + val isPolicyAgreement: State = _policyAgreement + + + + private val isGYUIDValid = mutableStateOf(false) + var oneKeyPreLogin: OnekeyPreLogin? = null + + + fun setUserName(loginName: String) { + userName.value = loginName + } + + fun setCaptcha(loginCaptcha: String) { + captcha.value = loginCaptcha + } + + fun setIsPolicyAgreement(isAgreement: Boolean) { + _policyAgreement.value = isAgreement + } + + + + private val _isLogin = mutableStateOf(false) + val isLogin: State = _isLogin + + fun setLogin(isLogin: Boolean) { + _isLogin.value = isLogin + } + + fun oneKeyLoginForGeTuiSdk(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) { + // 初始化 SDK + GYManager.getInstance().init(GyConfig.with(activity.applicationContext).callBack(object : GyCallBack { + override fun onSuccess(response: GYResponse?) { + isGYUIDValid.value = true + Log.i(ONEKEY_TAG, "--->初始化: onSuccess") + + //预登录(提高拉起速度,可选但推荐) + GYManager.getInstance().ePreLogin(/* timeout = */ 8000, /* gyCallBack = */ object : GyCallBack { + override fun onSuccess(response: GYResponse?) { + Log.i(ONEKEY_TAG, "--->预登录: onSuccess--->${response}") + val preLoginData = response?.msg + if (!preLoginData.isNullOrEmpty()) { + try { + val json = Json { ignoreUnknownKeys = true } + oneKeyPreLogin = + json.decodeFromString(preLoginData) + + // 打印解析后的详细数据 + Log.i(ONEKEY_TAG, "=== 预登录数据解析结果 ===") + Log.i(ONEKEY_TAG, "流程ID: ${oneKeyPreLogin?.process_id}") + Log.i(ONEKEY_TAG, "运营商类型: ${oneKeyPreLogin?.operatorType}") + Log.i(ONEKEY_TAG, "客户端类型: ${oneKeyPreLogin?.clienttype}") + Log.i(ONEKEY_TAG, "访问码: ${oneKeyPreLogin?.accessCode}") + Log.i(ONEKEY_TAG, "手机号: ${oneKeyPreLogin?.number}") + Log.i(ONEKEY_TAG, "过期时间: ${oneKeyPreLogin?.expiredTime}") + Log.i(ONEKEY_TAG, "错误码: ${oneKeyPreLogin?.errorCode}") + Log.i(ONEKEY_TAG, "错误描述: ${oneKeyPreLogin?.errorDesc}") + Log.i(ONEKEY_TAG, "耗时: ${oneKeyPreLogin?.costTime}ms") + Log.i(ONEKEY_TAG, "================================") + + // 根据解析结果决定是否继续(根据errorCode判断) + if (oneKeyPreLogin?.errorCode == 0) { + oneKeyLoginValid( + activity = activity, + onShowOneKeyScreen = onShowOneKeyScreen + ) + } else { + onShowOneKeyScreen(false) + Log.e(ONEKEY_TAG, "预登录校验失败: ${oneKeyPreLogin?.errorDesc}") + } + + } catch (e: Exception) { + Log.e(ONEKEY_TAG, "JSON解析失败: ${e.message}") + Log.e(ONEKEY_TAG, "原始数据: $preLoginData") + // 解析失败时继续执行原逻辑 + onShowOneKeyScreen(false) + } + } + } + + override fun onFailed(response: GYResponse?) { + onShowOneKeyScreen(false) + Log.i(ONEKEY_TAG, "--->预登录: onFailed--->${response}") + } + }) + } + + override fun onFailed(response: GYResponse?) { + onShowOneKeyScreen(false) + Log.i(ONEKEY_TAG, "--->初始化: onFailed--->${response}") + } + }).build()) + } + + private fun oneKeyLoginValid(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) { + if (GYManager.getInstance().isPreLoginResultValid) { + //预登录有效,启动登录授权页 + onShowOneKeyScreen(true) + Log.i(ONEKEY_TAG, "--->预登录校验有效A: onSuccess") + } else { + //考虑到是用户在等待,建议超时8s以上,至少设置5s以上 + GYManager.getInstance().ePreLogin(5000, object : GyCallBack { + override fun onSuccess(response: GYResponse?) { + //预登录成功,启动登录授权页 + onShowOneKeyScreen(true) + Log.i(ONEKEY_TAG, "--->预登录校验有效B: onSuccess--->${response}") + } + + override fun onFailed(response: GYResponse?) { + //预登录失败,提示用户稍后重试 + onShowOneKeyScreen(false) + Log.i(ONEKEY_TAG, "--->预登录校验有效B: onFailed--->${response}") + } + }) + } + } + + + /** + * 请求验证码 + */ + fun requestCaptcha() { + // TODO: 发送请求获取验证码 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/SplashViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/SplashViewModel.kt new file mode 100644 index 0000000..e071583 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/SplashViewModel.kt @@ -0,0 +1,11 @@ +package com.img.rabbit.viewmodel + +import androidx.lifecycle.ViewModel + +class SplashViewModel : ViewModel() { + val isLoading = androidx.compose.runtime.mutableStateOf(true) + + fun setLoading(loading: Boolean) { + isLoading.value = loading + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_circle_checkbox.xml b/app/src/main/res/drawable/selector_circle_checkbox.xml new file mode 100644 index 0000000..b126681 --- /dev/null +++ b/app/src/main/res/drawable/selector_circle_checkbox.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_252525_cor359.xml b/app/src/main/res/drawable/shape_252525_cor359.xml new file mode 100644 index 0000000..9867756 --- /dev/null +++ b/app/src/main/res/drawable/shape_252525_cor359.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_one_key_login.xml b/app/src/main/res/layout/layout_one_key_login.xml new file mode 100644 index 0000000..baed9f5 --- /dev/null +++ b/app/src/main/res/layout/layout_one_key_login.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-xxhdpi/ic_account_switch_top_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_account_switch_top_mask.webp new file mode 100644 index 0000000..88105a2 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_account_switch_top_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_alipay.webp b/app/src/main/res/mipmap-xxhdpi/ic_alipay.webp new file mode 100644 index 0000000..68fcadf Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_alipay.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_arrow_right.webp b/app/src/main/res/mipmap-xxhdpi/ic_arrow_right.webp new file mode 100644 index 0000000..6bde034 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_arrow_right.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_back.webp b/app/src/main/res/mipmap-xxhdpi/ic_back.webp new file mode 100644 index 0000000..2f67b80 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_back.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_camera.png b/app/src/main/res/mipmap-xxhdpi/ic_camera.png new file mode 100644 index 0000000..555449e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_camera.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_camera_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_camera_mask.webp new file mode 100644 index 0000000..00edf6e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_camera_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_1.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_1.png new file mode 100644 index 0000000..e412913 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_1.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_2.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_2.png new file mode 100644 index 0000000..e40c7cf Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_2.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_3.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_3.png new file mode 100644 index 0000000..0668071 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_3.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_4.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_4.png new file mode 100644 index 0000000..4c6f814 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_4.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_5.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_5.png new file mode 100644 index 0000000..6260250 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_5.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_6.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_6.png new file mode 100644 index 0000000..dfc58ec Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_female_6.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_1.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_1.png new file mode 100644 index 0000000..5fb5a90 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_1.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_2.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_2.png new file mode 100644 index 0000000..ff1df2f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_2.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_3.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_3.png new file mode 100644 index 0000000..587b5b9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_3.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_4.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_4.png new file mode 100644 index 0000000..5397082 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_4.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_5.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_5.png new file mode 100644 index 0000000..d09840e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_5.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_6.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_6.png new file mode 100644 index 0000000..d116699 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_6.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_7.png b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_7.png new file mode 100644 index 0000000..6c44d60 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clothing_man_7.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_cutout_base_color_tag.webp b/app/src/main/res/mipmap-xxhdpi/ic_cutout_base_color_tag.webp new file mode 100644 index 0000000..8133ea3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_cutout_base_color_tag.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_cutout_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_cutout_bg.webp new file mode 100644 index 0000000..b00563d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_cutout_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_cutout_clothing.webp b/app/src/main/res/mipmap-xxhdpi/ic_cutout_clothing.webp new file mode 100644 index 0000000..1307886 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_cutout_clothing.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_cutout_color_less.webp b/app/src/main/res/mipmap-xxhdpi/ic_cutout_color_less.webp new file mode 100644 index 0000000..37135c0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_cutout_color_less.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_cutout_hairstyle.webp b/app/src/main/res/mipmap-xxhdpi/ic_cutout_hairstyle.webp new file mode 100644 index 0000000..13f64b0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_cutout_hairstyle.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_cutout_size.webp b/app/src/main/res/mipmap-xxhdpi/ic_cutout_size.webp new file mode 100644 index 0000000..4a61013 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_cutout_size.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_dialog_top_mask1.webp b/app/src/main/res/mipmap-xxhdpi/ic_dialog_top_mask1.webp new file mode 100644 index 0000000..dcbd947 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_dialog_top_mask1.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_gallery.png b/app/src/main/res/mipmap-xxhdpi/ic_gallery.png new file mode 100644 index 0000000..1efe409 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_gallery.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_1.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_1.png new file mode 100644 index 0000000..7b50c24 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_1.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_2.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_2.png new file mode 100644 index 0000000..d4b7b8d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_2.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_3.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_3.png new file mode 100644 index 0000000..9c1f809 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_3.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_4.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_4.png new file mode 100644 index 0000000..68bb66e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_4.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_5.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_5.png new file mode 100644 index 0000000..dfa6830 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_5.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_6.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_6.png new file mode 100644 index 0000000..413ce09 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_female_6.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_1.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_1.png new file mode 100644 index 0000000..c6dba1a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_1.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_2.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_2.png new file mode 100644 index 0000000..284d32a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_2.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_3.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_3.png new file mode 100644 index 0000000..b9ca718 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_3.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_4.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_4.png new file mode 100644 index 0000000..98c3a59 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_4.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_5.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_5.png new file mode 100644 index 0000000..a57d95b Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_5.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_6.png b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_6.png new file mode 100644 index 0000000..05e2068 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hairstyle_man_6.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_hide.webp b/app/src/main/res/mipmap-xxhdpi/ic_hide.webp new file mode 100644 index 0000000..e2fa3d5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_hide.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_certificate_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_certificate_bg.webp new file mode 100644 index 0000000..83a2a29 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_certificate_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_normal.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_normal.webp new file mode 100644 index 0000000..96678ee Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_normal.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_other_1_puzzle.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_other_1_puzzle.webp new file mode 100644 index 0000000..a30973d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_other_1_puzzle.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_other_2_format.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_other_2_format.webp new file mode 100644 index 0000000..fe71a7e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_other_2_format.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_other_3_size.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_other_3_size.webp new file mode 100644 index 0000000..8c9e2e8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_other_3_size.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_other_4_camera.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_other_4_camera.webp new file mode 100644 index 0000000..36a2eda Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_other_4_camera.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_selected.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_selected.webp new file mode 100644 index 0000000..642f5ea Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_selected.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_size_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_size_bg.webp new file mode 100644 index 0000000..5083294 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_size_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_title_1_size.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_title_1_size.webp new file mode 100644 index 0000000..4daeabb Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_title_1_size.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_title_2_certificate.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_title_2_certificate.webp new file mode 100644 index 0000000..6ddd00d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_title_2_certificate.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_title_3_other.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_title_3_other.webp new file mode 100644 index 0000000..0d3f421 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_title_3_other.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_top_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_top_mask.webp new file mode 100644 index 0000000..52e68cd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_top_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_image_empty_pld.webp b/app/src/main/res/mipmap-xxhdpi/ic_image_empty_pld.webp new file mode 100644 index 0000000..619b8b7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_image_empty_pld.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_info.webp b/app/src/main/res/mipmap-xxhdpi/ic_info.webp new file mode 100644 index 0000000..2d89499 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_info.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_logo.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_logo.png new file mode 100644 index 0000000..8f8a39a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_logo.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_loading.png b/app/src/main/res/mipmap-xxhdpi/ic_loading.png new file mode 100644 index 0000000..707eea4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_loading.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_main_previous_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_main_previous_mask.webp new file mode 100644 index 0000000..4785c9d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_main_previous_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_feedback.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_feedback.webp new file mode 100644 index 0000000..f4a4262 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_feedback.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_normal.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_normal.webp new file mode 100644 index 0000000..dba830a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_normal.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_open.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_open.webp new file mode 100644 index 0000000..a27f6b1 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_open.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_selected.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_selected.webp new file mode 100644 index 0000000..f7497cf Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_selected.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_service.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_service.webp new file mode 100644 index 0000000..012ac12 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_service.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_setting.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_setting.webp new file mode 100644 index 0000000..91dff07 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_setting.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_top_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_top_mask.webp new file mode 100644 index 0000000..4b33850 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_top_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_update.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_update.webp new file mode 100644 index 0000000..2adad09 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_update.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_vip_bar_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_vip_bar_bg.webp new file mode 100644 index 0000000..d640731 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_vip_bar_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_vip_bar_bg1.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_vip_bar_bg1.webp new file mode 100644 index 0000000..0aca27f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_vip_bar_bg1.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_network_disconnected.webp b/app/src/main/res/mipmap-xxhdpi/ic_network_disconnected.webp new file mode 100644 index 0000000..70467a6 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_network_disconnected.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_normal_top_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_normal_top_mask.webp new file mode 100644 index 0000000..431a5de Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_normal_top_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_phone.webp b/app/src/main/res/mipmap-xxhdpi/ic_phone.webp new file mode 100644 index 0000000..015e7f1 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_phone.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_photo_sample.webp b/app/src/main/res/mipmap-xxhdpi/ic_photo_sample.webp new file mode 100644 index 0000000..bf3de48 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_photo_sample.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_picture_add.webp b/app/src/main/res/mipmap-xxhdpi/ic_picture_add.webp new file mode 100644 index 0000000..72834d2 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_picture_add.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_picture_del.png b/app/src/main/res/mipmap-xxhdpi/ic_picture_del.png new file mode 100644 index 0000000..ef65700 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_picture_del.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_privacy_policy_top_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_privacy_policy_top_mask.webp new file mode 100644 index 0000000..dcbd947 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_privacy_policy_top_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_splash_logo.webp b/app/src/main/res/mipmap-xxhdpi/ic_splash_logo.webp new file mode 100644 index 0000000..d975f46 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_splash_logo.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_splash_mask.webp b/app/src/main/res/mipmap-xxhdpi/ic_splash_mask.webp new file mode 100644 index 0000000..bea3c58 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_splash_mask.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_tick_circle_checked.webp b/app/src/main/res/mipmap-xxhdpi/ic_tick_circle_checked.webp new file mode 100644 index 0000000..5042b3a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_tick_circle_checked.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_tick_circle_normal.webp b/app/src/main/res/mipmap-xxhdpi/ic_tick_circle_normal.webp new file mode 100644 index 0000000..514d975 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_tick_circle_normal.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_user_avatar_default.webp b/app/src/main/res/mipmap-xxhdpi/ic_user_avatar_default.webp new file mode 100644 index 0000000..fa9bc16 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_user_avatar_default.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vertical_divider_dotted_line.webp b/app/src/main/res/mipmap-xxhdpi/ic_vertical_divider_dotted_line.webp new file mode 100644 index 0000000..5077fa9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vertical_divider_dotted_line.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_view.webp b/app/src/main/res/mipmap-xxhdpi/ic_view.webp new file mode 100644 index 0000000..2392529 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_view.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_wechat.webp b/app/src/main/res/mipmap-xxhdpi/ic_wechat.webp new file mode 100644 index 0000000..11671a7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_wechat.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..ac236c3 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #FF000000 + #FFFFFFFF + + #00000000 + + #00000000 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..65b1cb8 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 截图兔 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ade2a10 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/filepath_data.xml b/app/src/main/res/xml/filepath_data.xml new file mode 100644 index 0000000..d551921 --- /dev/null +++ b/app/src/main/res/xml/filepath_data.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..b4582a3 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + enrichgw.10010.com + + \ No newline at end of file diff --git a/app/src/test/java/com/img/rabbit/ExampleUnitTest.kt b/app/src/test/java/com/img/rabbit/ExampleUnitTest.kt new file mode 100644 index 0000000..737ddfd --- /dev/null +++ b/app/src/test/java/com/img/rabbit/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.img.rabbit + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0b78e49 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,7 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.org.jetbrains.kotlin.android) apply false + alias(libs.plugins.compose.compiler) apply false + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..b5f5bc4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,33 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true + +# ???? +GETUI_APPID=40qbPjPkYs7TnVAYCX0Ig6 +GT_INSTALL_CHANNEL=general + +# ???keystore +RELEASE_KEY_PASSWORD=sQYG1Jee +RELEASE_KEY_ALIAS=__uni__7e100bb +RELEASE_STORE_PASSWORD=sQYG1Jee +RELEASE_STORE_FILE=bidinfo.keystore \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..445f5ec --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,88 @@ +[versions] +#plugins +agp = "9.0.0" +kotlin = "2.3.0" + +#AndroidX Basic Version +coreKtx = "1.13.1" +appcompat = "1.7.0" +kotlinxSerializationJson = "1.10.0" +material = "1.10.0" +activity = "1.9.0" +constraintlayout = "2.1.4" +splashscreen = "1.0.1" +datastoreCore = "1.2.0" +datastorePreferences = "1.1.1" + +# Compose Version +composeBom = "2024.05.00" +composeLifecycle = "2.6.2" + + +#Junit Version +junit = "4.13.2" +androidx-test-ext-junit = "1.1.5" +androidx-test-espresso-core = "3.5.1" +navigationRuntimeKtx = "2.9.7" + +# Third-party version +gson = "2.13.2" +gysdk = "3.2.3.0" +runtimeLivedata = "1.10.2" +mmkv = "2.3.0" +navigationCompose = "2.9.7" +coilCompose = "3.3.0" +segmentationSelfie = "16.0.0-beta4" +faceDetection = "16.1.5" +foundation = "1.10.2" +androidGifDrawableEncoder = "1.2.30" +gifeEncoder = "0.10.1" + + +[libraries] +#AndroidX Basic dependencies +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-material = { module = "com.google.android.material:material", version.ref = "material" } +androidx-activity = { module = "androidx.activity:activity", version.ref = "activity" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } + +# Compose dependencies +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-activity-compose = { module = "androidx.activity:activity-compose" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "composeLifecycle" } +androidx-compose-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "composeLifecycle" } +androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } + +#Test dependencies +junit = { module = "junit:junit", version.ref = "junit" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" } +androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" } + +# Third-party dependencies +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +gysdk = { module = "com.getui:gysdk", version.ref = "gysdk" } +mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" } +segmentation-selfie = { module = "com.google.mlkit:segmentation-selfie", version.ref = "segmentationSelfie" } +face-detection = { module = "com.google.mlkit:face-detection", version.ref = "faceDetection" } +androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } +android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawableEncoder" } +gif-encoder = { module = "com.squareup:gifencoder", version.ref = "gifeEncoder" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c3af895 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +#Mon Feb 02 16:01:00 CST 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..ef07e01 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7da8671 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,27 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + maven { url = uri("https://repo1.maven.org/maven2/") } + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } + maven { url = uri("https://mvn.getui.com/nexus/content/repositories/releases") } + } +} + +rootProject.name = "rabbit" +include(":app")