From d2a3e61b54e4cf7b4b561f7959fa2570cff43ce5 Mon Sep 17 00:00:00 2001 From: deepfal Date: Mon, 18 Aug 2025 11:17:38 +0800 Subject: [PATCH] init --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/.name | 1 + .idea/AndroidProjectSystem.xml | 6 + .idea/appInsightsSettings.xml | 6 + .idea/codeStyles/Project.xml | 123 ++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 26 + .idea/deviceManager.xml | 13 + .idea/gradle.xml | 19 + .idea/inspectionProfiles/Project_Default.xml | 50 + .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 17 + .idea/vcs.xml | 6 + REFACTOR_SUMMARY.md | 106 ++ app/.gitignore | 1 + app/build.gradle.kts | 128 ++ app/keystore/band_keystore.jks | Bin 0 -> 2355 bytes app/keystore/band_keystore.p12 | Bin 0 -> 2834 bytes app/keystore/keystore.jks | Bin 0 -> 2593 bytes app/libs/xms-wearable-lib_1.4_release.aar | Bin 0 -> 102453 bytes app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 44 + .../band/TOTPauthenticator/MainActivity.kt | 1543 +++++++++++++++++ .../components/AddComponents.kt | 353 ++++ .../components/ModernTOTPCard.kt | 841 +++++++++ .../components/ReorderableBandAccountsList.kt | 175 ++ .../components/SyncScreen.kt | 1138 ++++++++++++ .../band/TOTPauthenticator/data/TOTPInfo.kt | 40 + .../scanner/QRCodeScanner.kt | 370 ++++ .../TOTPauthenticator/sync/SyncManager.kt | 129 ++ .../band/TOTPauthenticator/ui/theme/Color.kt | 11 + .../band/TOTPauthenticator/ui/theme/Theme.kt | 59 + .../band/TOTPauthenticator/ui/theme/Type.kt | 17 + .../utils/LocalAccountManager.kt | 160 ++ .../TOTPauthenticator/utils/TOTPGenerator.kt | 198 +++ .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 4561 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 3026 bytes .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5307 bytes .../ic_launcher_foreground.png | Bin 0 -> 7231 bytes .../ic_launcher_foreground.png | Bin 0 -> 10157 bytes .../res/drawable/ic_launcher_background.xml | 10 + .../res/drawable/ic_launcher_foreground.png | Bin 0 -> 3026 bytes app/src/main/res/layout/activity_main.xml | 33 + app/src/main/res/layout/content_main.xml | 19 + app/src/main/res/layout/fragment_first.xml | 35 + app/src/main/res/layout/fragment_second.xml | 35 + app/src/main/res/menu/menu_main.xml | 10 + .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1500 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 1500 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1194 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1194 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1440 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 1440 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2290 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 2290 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 2604 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 2604 bytes app/src/main/res/navigation/nav_graph.xml | 28 + app/src/main/res/values-land/dimens.xml | 3 + app/src/main/res/values-night/themes.xml | 7 + app/src/main/res/values-v23/themes.xml | 9 + app/src/main/res/values-w1240dp/dimens.xml | 3 + app/src/main/res/values-w600dp/dimens.xml | 3 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 46 + app/src/main/res/values/themes.xml | 9 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../band/TOTPauthenticator/ExampleUnitTest.kt | 17 + appkeystoreband_keystore.jks | Bin 0 -> 2355 bytes build.gradle.kts | 5 + gradle.properties | 23 + gradle/libs.versions.toml | 56 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++ gradlew.bat | 89 + keystore.properties | 11 + keystore/keystore.jks | Bin 0 -> 2593 bytes keystoreband_keystore.jks | Bin 0 -> 2355 bytes keystoreband_keystore.p12 | Bin 0 -> 2834 bytes settings.gradle.kts | 23 + 89 files changed, 6390 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/appInsightsSettings.xml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 REFACTOR_SUMMARY.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/keystore/band_keystore.jks create mode 100644 app/keystore/band_keystore.p12 create mode 100644 app/keystore/keystore.jks create mode 100644 app/libs/xms-wearable-lib_1.4_release.aar create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/cn/deepfal/band/TOTPauthenticator/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/MainActivity.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/AddComponents.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ModernTOTPCard.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ReorderableBandAccountsList.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/SyncScreen.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/data/TOTPInfo.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/scanner/QRCodeScanner.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/sync/SyncManager.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Color.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Theme.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Type.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/LocalAccountManager.kt create mode 100644 app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/TOTPGenerator.kt create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.png create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/content_main.xml create mode 100644 app/src/main/res/layout/fragment_first.xml create mode 100644 app/src/main/res/layout/fragment_second.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values-land/dimens.xml create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values-v23/themes.xml create mode 100644 app/src/main/res/values-w1240dp/dimens.xml create mode 100644 app/src/main/res/values-w600dp/dimens.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/cn/deepfal/band/TOTPauthenticator/ExampleUnitTest.kt create mode 100644 appkeystoreband_keystore.jks create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 keystore.properties create mode 100644 keystore/keystore.jks create mode 100644 keystoreband_keystore.jks create mode 100644 keystoreband_keystore.p12 create mode 100644 settings.gradle.kts 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..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..886b16c --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +BandTOTP-APP \ 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/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..6bbe2ae --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ 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..41096b2 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ 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..f0c6ad0 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,50 @@ + + + + \ 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/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/REFACTOR_SUMMARY.md b/REFACTOR_SUMMARY.md new file mode 100644 index 0000000..bfb17e7 --- /dev/null +++ b/REFACTOR_SUMMARY.md @@ -0,0 +1,106 @@ +# 🎯 现代化TOTP验证器应用重构完成 + +## ✨ 重构成果 + +已成功将原有的TOTP验证器应用重构为现代化、优雅的身份验证器应用,完全符合您的设计要求。 + +### 🎨 界面重构亮点 + +#### **主界面设计** +- **极简风格**:去除复杂的手环状态显示,专注于验证码展示 +- **Google Authenticator风格**:采用Material Design 3设计语言 +- **现代化图标**:使用Material Icons替代emoji,确保跨设备一致性 +- **优雅动画**:流畅的页面切换和列表动画效果 + +#### **TOTP卡片设计** +- **服务图标**:智能识别服务并显示对应图标和品牌色彩 +- **实时倒计时**:圆形进度条,剩余5秒时变红警告 +- **验证码显示**:等宽字体,分组显示,支持Steam特殊格式 +- **类型标签**:清晰标识TOTP/Steam类型 + +### 🚀 功能增强 + +#### **添加方式升级** +- **📱 浮动按钮**:右下角双FAB设计,符合Material规范 +- **📷 二维码扫描**:集成CameraX + MLKit,专业扫描界面 +- **📁 文件导入**:保留原有功能,现代化界面 +- **✏️ 手动添加**:完整参数配置,支持高级选项 + +#### **同步功能重构** +- **🔄 智能同步界面**:统一列表显示,清晰状态标识 +- **📊 三种状态**: + - 📱 仅手机(蓝色) + - ⌚ 仅手环(橙色) + - ✅ 已同步(绿色) +- **🎯 批量操作**:支持全部同步到手机/手环 +- **📈 统计面板**:实时显示各状态账户数量 + +### 🛠️ 技术升级 + +#### **现代化依赖** +```kotlin +// Material Icons扩展包 +implementation("androidx.compose.material:material-icons-extended:1.6.0") + +// 相机和扫码 +implementation("androidx.camera:camera-camera2:1.3.1") +implementation("com.google.mlkit:barcode-scanning:17.2.0") + +// 权限处理 +implementation("com.google.accompanist:accompanist-permissions:0.32.0") +``` + +#### **架构优化** +- **组件化设计**:功能模块独立,便于维护 +- **状态管理**:使用Compose State进行响应式UI更新 +- **现代API**:CameraX + MLKit + Material Design 3 + +### 📁 项目结构 + +``` +app/src/main/java/cn/deepfal/band/TOTPauthenticator/ +├── MainActivity.kt # 主Activity - 现代化设计 +├── data/ +│ └── TOTPInfo.kt # TOTP数据模型 +├── components/ +│ ├── ModernTOTPCard.kt # 现代化验证码卡片 +│ ├── AddComponents.kt # 添加相关组件 +│ └── SyncScreen.kt # 同步管理界面 +├── scanner/ +│ └── QRCodeScanner.kt # 专业二维码扫描器 +├── sync/ +│ └── SyncManager.kt # 同步逻辑管理 +└── utils/ + ├── LocalAccountManager.kt # 本地存储管理 + └── TOTPGenerator.kt # TOTP算法实现 +``` + +### 🎯 用户体验提升 + +#### **主界面** +- 空状态友好提示 +- 流畅的账户列表滚动 +- 实时验证码更新 +- 智能色彩和图标识别 + +#### **添加流程** +- 直观的底部菜单 +- 专业的扫码体验 +- 完整的手动输入选项 +- 即时反馈和错误处理 + +#### **同步管理** +- 清晰的状态可视化 +- 一键批量操作 +- 实时连接状态监控 +- 友好的错误提示 + +## 🏆 设计目标达成 + +✅ **极简美观**:去除冗余元素,专注核心功能 +✅ **现代审美**:Material Design 3,Google Authenticator风格 +✅ **功能完整**:扫码、手动、文件导入三种添加方式 +✅ **智能同步**:可视化状态管理,去重合并 +✅ **专业体验**:真实图标、品牌色彩、流畅动画 + +应用现已完成现代化重构,构建成功,可以直接使用和测试!🎉 \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..fbfcd15 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,128 @@ +import java.util.Properties + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) +} + +// 加载签名配置 - 参考官方demo +val keystorePropertiesFile = rootProject.file("keystore.properties") +val keystoreProperties = Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(keystorePropertiesFile.inputStream()) +} + +android { + namespace = "cn.deepfal.band.TOTPauthenticator" + compileSdk = 36 + + defaultConfig { + applicationId = "cn.deepfal.band.TOTPauthenticator" + minSdk = 28 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + // 签名配置 - 参考官方demo + signingConfigs { + getByName("debug") { + if (keystoreProperties.isNotEmpty()) { + keyAlias = keystoreProperties["debug.key.alias"].toString() + keyPassword = keystoreProperties["debug.key.password"].toString() + storeFile = file(keystoreProperties["debug.store.file"].toString()) + storePassword = keystoreProperties["debug.store.password"].toString() + } + } + create("release") { + if (keystoreProperties.isNotEmpty()) { + keyAlias = keystoreProperties["release.key.alias"].toString() + keyPassword = keystoreProperties["release.key.password"].toString() + storeFile = file(keystoreProperties["release.store.file"].toString()) + storePassword = keystoreProperties["release.store.password"].toString() + } + } + } + + buildTypes { + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + } + release { + isMinifyEnabled = false + signingConfig = signingConfigs.getByName("release") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + + // 小米可穿戴SDK + implementation(files("libs/xms-wearable-lib_1.4_release.aar")) + + // Compose相关依赖 + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + + // Material Icons - 现代化图标 + implementation("androidx.compose.material:material-icons-extended:1.6.0") + + // 相机和二维码扫描 + implementation("androidx.camera:camera-camera2:1.3.1") + implementation("androidx.camera:camera-lifecycle:1.3.1") + implementation("androidx.camera:camera-view:1.3.1") + implementation("com.google.mlkit:barcode-scanning:17.2.0") + + // 权限处理 + implementation("com.google.accompanist:accompanist-permissions:0.32.0") + + // 文件选择器 + implementation("androidx.activity:activity-compose:1.8.2") + + // 拖拽排序 + implementation("org.burnoutcrew.composereorderable:reorderable:0.9.6") + + // Coroutines + implementation(libs.kotlinx.coroutines.android) + + // Gson for JSON + implementation(libs.gson) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/app/keystore/band_keystore.jks b/app/keystore/band_keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..d86dd5055b56dbeabf7e3b31cb11e6265707b5c6 GIT binary patch literal 2355 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 z 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 literal 0 HcmV?d00001 diff --git a/app/libs/xms-wearable-lib_1.4_release.aar b/app/libs/xms-wearable-lib_1.4_release.aar new file mode 100644 index 0000000000000000000000000000000000000000..3291866c0e6869ae529400db4e79652b48f59b6c GIT binary patch literal 102453 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 K*gJuB6axS!_X=VF literal 0 HcmV?d00001 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/app/src/androidTest/java/cn/deepfal/band/TOTPauthenticator/ExampleInstrumentedTest.kt b/app/src/androidTest/java/cn/deepfal/band/TOTPauthenticator/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..984401e --- /dev/null +++ b/app/src/androidTest/java/cn/deepfal/band/TOTPauthenticator/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package cn.deepfal.band.TOTPauthenticator + +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("cn.deepfal.band.TOTPauthenticator", 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..deaba07 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/MainActivity.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/MainActivity.kt new file mode 100644 index 0000000..1333b64 --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/MainActivity.kt @@ -0,0 +1,1543 @@ +package cn.deepfal.band.TOTPauthenticator + +import android.Manifest +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import androidx.core.view.WindowCompat +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.OnBackPressedCallback +import androidx.compose.animation.* +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import cn.deepfal.band.TOTPauthenticator.ui.theme.BandTOTPAPPTheme +import cn.deepfal.band.TOTPauthenticator.data.TOTPInfo +import cn.deepfal.band.TOTPauthenticator.utils.LocalAccountManager +import cn.deepfal.band.TOTPauthenticator.sync.SyncManager +import cn.deepfal.band.TOTPauthenticator.scanner.QRCodeScanner +import cn.deepfal.band.TOTPauthenticator.components.ModernTOTPCard +import cn.deepfal.band.TOTPauthenticator.components.AddTokenMenu +import cn.deepfal.band.TOTPauthenticator.components.ManualAddDialog +import cn.deepfal.band.TOTPauthenticator.components.SyncScreen +import cn.deepfal.band.TOTPauthenticator.components.AccountDetailsDialog +import com.xiaomi.xms.wearable.Wearable +import com.xiaomi.xms.wearable.auth.Permission +import com.xiaomi.xms.wearable.message.MessageApi +import com.xiaomi.xms.wearable.message.OnMessageReceivedListener +import com.xiaomi.xms.wearable.node.Node +import com.xiaomi.xms.wearable.node.NodeApi +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay +import org.json.JSONObject +import java.io.BufferedReader +import java.io.InputStreamReader +import org.burnoutcrew.reorderable.* + +class MainActivity : ComponentActivity() { + + private lateinit var nodeApi: NodeApi + private lateinit var messageApi: MessageApi + private lateinit var localAccountManager: LocalAccountManager + private lateinit var syncManager: SyncManager + private val handler = Handler(Looper.getMainLooper()) + + // 手环连接相关 + private lateinit var nodeId: String + private lateinit var curNode: Node + + // 状态管理 + private var accountsState = mutableStateOf>(emptyList()) + private var bandAccountsState = mutableStateOf>(emptyList()) + private var showSyncScreen = mutableStateOf(false) + private var showAddMenu = mutableStateOf(false) + private var showQRScanner = mutableStateOf(false) + private var showManualAdd = mutableStateOf(false) + private var connectionStatusState = mutableStateOf("设备未连接") + private var isConnectedState = mutableStateOf(false) + private var syncLogsState = mutableStateOf>(emptyList()) + private var showAccountDetails = mutableStateOf(false) + private var selectedAccount = mutableStateOf(null) + private var isReorderMode = mutableStateOf(false) + + // 消息监听器状态 + private var isListenerRegistered = false + + // 消息监听器 + private val messageListener = OnMessageReceivedListener { deviceId, message -> + try { + val jsonString = String(message) + log("📨 收到手环消息: ${jsonString.take(100)}...") + + val jsonObject = JSONObject(jsonString) + if (jsonObject.has("list")) { + val accountsList = mutableListOf() + val jsonArray = jsonObject.getJSONArray("list") + + for (i in 0 until jsonArray.length()) { + val accountJson = jsonArray.getJSONObject(i) + val totpInfo = TOTPInfo( + name = accountJson.getString("name"), + usr = accountJson.getString("usr"), + key = accountJson.getString("key"), + algorithm = accountJson.optString("algorithm", "SHA1"), + digits = accountJson.optInt("digits", 6), + period = accountJson.optInt("period", 30), + type = accountJson.optString("type", "totp") + ) + accountsList.add(totpInfo) + } + + bandAccountsState.value = accountsList + log("✅ 成功接收到 ${accountsList.size} 个手环账户") + } + + // 处理删除确认 + if (jsonObject.has("cmd") && jsonObject.getString("cmd") == "deleteConfirmation") { + val deletedKey = jsonObject.getString("key") + val success = jsonObject.getBoolean("success") + + if (success) { + log("✅ 手环账户删除成功: ${deletedKey.take(8)}...") + Toast.makeText(this@MainActivity, "手环账户删除成功", Toast.LENGTH_SHORT).show() + + // 立即从bandAccountsState中移除删除的账户,提供即时UI反馈 + val currentBandAccounts = bandAccountsState.value.toMutableList() + val removed = currentBandAccounts.removeAll { it.key == deletedKey } + if (removed) { + bandAccountsState.value = currentBandAccounts + log("🔄 已立即更新UI,移除删除的账户") + } + + // 延迟重新请求手环账户列表以确保数据同步 + handler.postDelayed({ requestBandAccounts() }, 1000) + } else { + log("❌ 手环账户删除失败: ${deletedKey.take(8)}...") + Toast.makeText(this@MainActivity, "手环账户删除失败", Toast.LENGTH_SHORT).show() + } + } + + // 处理重排序确认 + if (jsonObject.has("cmd") && jsonObject.getString("cmd") == "reorderConfirmation") { + val success = jsonObject.getBoolean("success") + + if (success) { + log("✅ 手环账户重排序成功") + Toast.makeText(this@MainActivity, "手环账户重排序成功", Toast.LENGTH_SHORT).show() + + // 立即请求更新的手环账户列表 + requestBandAccounts() + } else { + log("❌ 手环账户重排序失败") + Toast.makeText(this@MainActivity, "手环账户重排序失败", Toast.LENGTH_SHORT).show() + } + } + } catch (e: Exception) { + log("❌ 解析手环消息失败: ${e.message}") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // 配置边到边显示 + enableEdgeToEdge() + + nodeApi = Wearable.getNodeApi(this) + messageApi = Wearable.getMessageApi(this) + localAccountManager = LocalAccountManager(this) + syncManager = SyncManager(this, nodeApi, messageApi) + + loadAccounts() + initializeWearableConnection() + + // 处理返回键逻辑 + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // 检查当前显示的界面状态,按优先级处理 + when { + // 账户详情对话框 + showAccountDetails.value -> { + showAccountDetails.value = false + } + // 手动添加对话框 + showManualAdd.value -> { + showManualAdd.value = false + } + // 二维码扫描器 + showQRScanner.value -> { + showQRScanner.value = false + } + // 添加菜单 + showAddMenu.value -> { + showAddMenu.value = false + } + // 主界面的重排序模式 + isReorderMode.value && !showSyncScreen.value -> { + isReorderMode.value = false + } + // 同步界面(包括同步界面内的重排序模式) + showSyncScreen.value -> { + showSyncScreen.value = false + } + else -> { + // 如果在主界面,则退出应用 + finish() + } + } + } + }) + + setContent { + BandTOTPAPPTheme { + // 根据主题动态设置状态栏内容颜色 + val isDarkTheme = androidx.compose.foundation.isSystemInDarkTheme() + + LaunchedEffect(isDarkTheme) { + WindowCompat.getInsetsController(window, window.decorView).apply { + isAppearanceLightStatusBars = !isDarkTheme // 浅色主题用深色状态栏内容,深色主题用浅色状态栏内容 + } + } + + MainScreen() + } + } + } + + override fun onDestroy() { + super.onDestroy() + // 清理消息监听器 + if (isListenerRegistered && ::nodeId.isInitialized) { + messageApi.removeListener(nodeId) + isListenerRegistered = false + log("🔄 已清理消息监听器") + } + } + + private fun initializeWearableConnection() { + // 先查询已连接的设备 + queryConnectedDevices() + } + + private fun queryConnectedDevices() { + connectionStatusState.value = "正在连接..." + isConnectedState.value = false + + nodeApi.connectedNodes.addOnSuccessListener { nodes -> + if (nodes.isNotEmpty()) { + curNode = nodes[0] + nodeId = curNode.id + connectionStatusState.value = "已连接: ${curNode.name}" + isConnectedState.value = true + log("🔗 已连接到设备: ${curNode.name}") + checkAndRequestPermissions() + } else { + connectionStatusState.value = "未找到设备" + isConnectedState.value = false + log("❌ 未找到连接的设备") + } + }.addOnFailureListener { e -> + connectionStatusState.value = "连接失败: ${e.message}" + isConnectedState.value = false + log("❌ 查询设备失败: ${e.message}") + } + } + + private fun checkAndRequestPermissions() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.BLUETOOTH), 1001) + } + + val authApi = Wearable.getAuthApi(this) + if (::nodeId.isInitialized) { + val did = nodeId + authApi.checkPermission(did, Permission.DEVICE_MANAGER) + .addOnSuccessListener { granted -> + if (!granted) { + authApi.requestPermission(did, Permission.DEVICE_MANAGER) + .addOnSuccessListener { + log("✅ 权限已获取") + setupMessageListener() + } + .addOnFailureListener { e -> + log("❌ 权限获取失败: ${e.message}") + } + } else { + log("✅ 权限已存在") + setupMessageListener() + } + } + .addOnFailureListener { e -> + log("❌ 权限检查失败: ${e.message}") + } + } + } + + private fun setupMessageListener() { + if (::nodeId.isInitialized) { + // 如果已经注册过监听器,先移除 + if (isListenerRegistered) { + messageApi.removeListener(nodeId) + log("🔄 已移除旧的消息监听器") + isListenerRegistered = false + } + + messageApi.addListener(nodeId, messageListener) + .addOnSuccessListener { + log("✅ 消息监听器已注册") + isListenerRegistered = true + // 自动请求一次账户列表 + handler.postDelayed({ + requestBandAccounts() + }, 2000) + } + .addOnFailureListener { e -> + log("❌ 消息监听器注册失败: ${e.message}") + isListenerRegistered = false + } + } + } + + private fun openWearableApp() { + if (::nodeId.isInitialized) { + log("🔄 正在打开手环应用...") + nodeApi.isWearAppInstalled(nodeId) + .addOnSuccessListener { isInstalled -> + if (isInstalled) { + nodeApi.launchWearApp(nodeId, "pages/index") + .addOnSuccessListener { + log("✅ 手环应用打开成功") + // 延迟请求账户,确保应用已启动 + handler.postDelayed({ + requestBandAccounts() + }, 3000) + } + .addOnFailureListener { e -> + log("❌ 打开手环应用失败: ${e.message}") + // 即使打开失败也尝试请求账户 + requestBandAccounts() + } + } else { + log("⚠️ 手环未安装TOTP应用") + Toast.makeText(this, "手环未安装TOTP应用,请先安装", Toast.LENGTH_SHORT).show() + } + } + .addOnFailureListener { e -> + log("❌ 检查手环应用安装状态失败: ${e.message}") + Toast.makeText(this, "检查手环应用失败: ${e.message}", Toast.LENGTH_SHORT).show() + } + } else { + log("❌ 设备未连接") + Toast.makeText(this, "设备未连接", Toast.LENGTH_SHORT).show() + } + } + + private fun requestBandAccounts() { + if (::nodeId.isInitialized) { + val requestMessage = """{"cmd":"getAccounts"}""" + log("📥 请求手环账户列表...") + + messageApi.sendMessage(nodeId, requestMessage.toByteArray()) + .addOnSuccessListener { + log("✅ 账户列表请求已发送") + } + .addOnFailureListener { e -> + log("❌ 账户列表请求失败: ${e.message}") + } + } else { + log("❌ 设备未连接") + } + } + + private fun deleteBandAccount(account: TOTPInfo) { + if (::nodeId.isInitialized) { + val deleteMessage = """{"cmd":"deleteAccount","key":"${account.key}"}""" + log("🗑️ 请求删除手环账户: ${account.name}") + + messageApi.sendMessage(nodeId, deleteMessage.toByteArray()) + .addOnSuccessListener { + log("✅ 删除请求已发送") + } + .addOnFailureListener { e -> + log("❌ 删除请求失败: ${e.message}") + Toast.makeText(this, "删除请求失败: ${e.message}", Toast.LENGTH_SHORT).show() + } + } else { + log("❌ 设备未连接,无法删除") + Toast.makeText(this, "设备未连接", Toast.LENGTH_SHORT).show() + } + } + + private fun reorderBandAccounts(reorderedAccounts: List) { + if (::nodeId.isInitialized) { + val reorderMessage = """{"cmd":"reorderAccounts","accounts":${reorderedAccounts.joinToString(",", "[", "]") { it.toJsonString() }}}""" + log("🔄 发送手环账户重排序命令...") + + // 立即更新本地状态以提供即时UI反馈 + bandAccountsState.value = reorderedAccounts + log("🔄 已立即更新UI,新顺序已生效") + + messageApi.sendMessage(nodeId, reorderMessage.toByteArray()) + .addOnSuccessListener { + log("✅ 重排序命令已发送") + Toast.makeText(this, "重排序命令已发送到手环", Toast.LENGTH_SHORT).show() + // 不需要延迟请求,手环确认后会自动刷新 + } + .addOnFailureListener { e -> + log("❌ 重排序命令失败: ${e.message}") + Toast.makeText(this, "重排序失败: ${e.message}", Toast.LENGTH_SHORT).show() + // 如果发送失败,重新请求手环账户以恢复原始顺序 + requestBandAccounts() + } + } else { + log("❌ 设备未连接,无法重排序") + Toast.makeText(this, "设备未连接", Toast.LENGTH_SHORT).show() + } + } + + private fun loadAccounts() { + try { + log("📂 开始加载本地账户...") + val accounts = localAccountManager.loadAccounts() + accountsState.value = accounts + log("✅ 成功加载 ${accounts.size} 个本地账户") + } catch (e: Exception) { + log("❌ 加载账户失败: ${e.message}") + log("📍 加载账户错误堆栈: ${e.stackTraceToString()}") + Toast.makeText(this, "加载账户失败: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun syncToWearable(accounts: List) { + if (::nodeId.isInitialized) { + val jsonMessage = """{"list":${accounts.joinToString(",", "[", "]") { it.toJsonString() }}}""" + log("📤 同步 ${accounts.size} 个账户到手环...") + + messageApi.sendMessage(nodeId, jsonMessage.toByteArray()) + .addOnSuccessListener { + log("✅ 账户同步成功") + Toast.makeText(this, "同步成功", Toast.LENGTH_SHORT).show() + // 重新请求手环账户验证同步结果 + handler.postDelayed({ requestBandAccounts() }, 1000) + } + .addOnFailureListener { e -> + log("❌ 账户同步失败: ${e.message}") + Toast.makeText(this, "同步失败: ${e.message}", Toast.LENGTH_SHORT).show() + } + } else { + log("❌ 设备未连接,无法同步") + Toast.makeText(this, "设备未连接", Toast.LENGTH_SHORT).show() + } + } + + private fun log(message: String) { + val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date()) + val logEntry = "$timestamp: $message" + syncLogsState.value = syncLogsState.value + logEntry + // 保持最新100条日志 + if (syncLogsState.value.size > 100) { + syncLogsState.value = syncLogsState.value.takeLast(100) + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun MainScreen() { + val accounts by accountsState + val showSync by showSyncScreen + val showAdd by showAddMenu + val showScanner by showQRScanner + val showManual by showManualAdd + + // 文件导入启动器 + val importFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri: Uri? -> + uri?.let { + handleFileImport(it) + } + } + + Box(modifier = Modifier.fillMaxSize()) { + // 主界面与同步界面的转场动画 + AnimatedVisibility( + visible = !showSync, + enter = slideInHorizontally( + initialOffsetX = { -it }, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + fadeIn(animationSpec = tween(durationMillis = 300)), + exit = slideOutHorizontally( + targetOffsetX = { -it }, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + fadeOut(animationSpec = tween(durationMillis = 300)) + ) { + // 主界面 + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .statusBarsPadding() + ) { + // 顶部标题栏 + TopAppBar( + title = { + // 标题文字切换动画 + AnimatedContent( + targetState = isReorderMode.value, + transitionSpec = { + slideInVertically { height -> -height } + fadeIn() togetherWith + slideOutVertically { height -> height } + fadeOut() + }, + label = "title_animation" + ) { isReorder -> + Text( + text = if (isReorder) "拖拽排序" else "身份验证器", + fontSize = 24.sp, + fontWeight = FontWeight.Medium + ) + } + }, + actions = { + if (accounts.isNotEmpty()) { + // 排序按钮动画 + AnimatedVisibility( + visible = true, + enter = scaleIn(animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)), + exit = scaleOut() + ) { + IconButton( + onClick = { + isReorderMode.value = !isReorderMode.value + }, + modifier = Modifier.animateContentSize() + ) { + // 图标切换动画 + AnimatedContent( + targetState = isReorderMode.value, + transitionSpec = { + scaleIn() + fadeIn() togetherWith scaleOut() + fadeOut() + }, + label = "icon_animation" + ) { isReorder -> + Icon( + imageVector = if (isReorder) Icons.Default.Check else Icons.Default.Reorder, + contentDescription = if (isReorder) "完成排序" else "排序" + ) + } + } + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) + + // 账户列表区域动画 + AnimatedContent( + targetState = accounts.isEmpty(), + transitionSpec = { + if (targetState) { + // 显示空状态 + fadeIn(animationSpec = tween(500)) togetherWith + fadeOut(animationSpec = tween(300)) + } else { + // 显示列表 + fadeIn(animationSpec = tween(500)) + + slideInVertically( + initialOffsetY = { it / 3 }, + animationSpec = tween(600, easing = EaseOutCubic) + ) togetherWith + fadeOut(animationSpec = tween(300)) + } + }, + label = "content_animation" + ) { isEmpty -> + if (isEmpty) { + EmptyState() + } else { + AccountsList( + accounts = accounts, + isReorderMode = isReorderMode.value, + onReorder = { reorderedList -> + accountsState.value = reorderedList + localAccountManager.saveAccounts(reorderedList) + log("📝 账户顺序已保存") + } + ) + } + } + } + } + + // 浮动操作按钮 - 移到外层Box中,只在主界面显示 + AnimatedVisibility( + visible = !showSync, + enter = slideInVertically( + initialOffsetY = { it }, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ) + fadeIn(), + exit = slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(200) + ) + fadeOut(), + modifier = Modifier.align(Alignment.BottomEnd) + ) { + FloatingActionButtons( + onAddClick = { showAddMenu.value = true }, + onSyncClick = { + showSyncScreen.value = true + // 打开同步界面时自动打开手环应用 + openWearableApp() + } + ) + } + + // 同步界面转场动画 + AnimatedVisibility( + visible = showSync, + enter = slideInHorizontally( + initialOffsetX = { it }, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + fadeIn(animationSpec = tween(durationMillis = 300)), + exit = slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + fadeOut(animationSpec = tween(durationMillis = 300)) + ) { + SyncScreen( + onClose = { showSyncScreen.value = false }, + phoneAccounts = accounts, + watchAccounts = bandAccountsState.value, + connectionStatus = connectionStatusState.value, + isConnected = isConnectedState.value, + syncLogs = syncLogsState.value, + onSyncToWatch = { accountsToSync -> + syncToWearable(accountsToSync) + }, + onSyncToPhone = { accountsToSync -> + accountsToSync.forEach { account -> + localAccountManager.addAccount(account) + } + loadAccounts() + log("✅ 同步 ${accountsToSync.size} 个账户到手机") + Toast.makeText(this@MainActivity, "同步到手机成功", Toast.LENGTH_SHORT).show() + }, + onRequestBandAccounts = { + openWearableApp() + }, + onReconnect = { + queryConnectedDevices() + }, + onDeleteFromBand = { account -> + deleteBandAccount(account) + }, + onReorderBandAccounts = { reorderedAccounts -> + reorderBandAccounts(reorderedAccounts) + } + ) + } + + // 添加菜单 - 简洁的从下滑入动画 + AnimatedVisibility( + visible = showAdd, + enter = slideInVertically( + initialOffsetY = { it }, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium + ) + ) + fadeIn(animationSpec = tween(durationMillis = 250)), + exit = slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(durationMillis = 200, easing = EaseInQuart) + ) + fadeOut(animationSpec = tween(durationMillis = 150)) + ) { + AddTokenMenu( + onDismiss = { showAddMenu.value = false }, + onScanQR = { + showAddMenu.value = false + showQRScanner.value = true + }, + onImportFile = { + showAddMenu.value = false + importFileLauncher.launch("text/plain") + }, + onManualAdd = { + showAddMenu.value = false + showManualAdd.value = true + } + ) + } + + // 二维码扫描器淡入动画 + AnimatedVisibility( + visible = showScanner, + enter = fadeIn(animationSpec = tween(300)) + scaleIn( + initialScale = 0.8f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ), + exit = fadeOut(animationSpec = tween(200)) + scaleOut(targetScale = 0.8f) + ) { + QRCodeScanner( + onCodeScanned = { qrCode -> + showQRScanner.value = false + handleQRCode(qrCode) + }, + onClose = { showQRScanner.value = false } + ) + } + + // 对话框动画 + AnimatedVisibility( + visible = showManual, + enter = fadeIn() + scaleIn( + initialScale = 0.8f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ), + exit = fadeOut() + scaleOut(targetScale = 0.8f) + ) { + ManualAddDialog( + onDismiss = { showManualAdd.value = false }, + onAdd = { totpInfo -> + localAccountManager.addAccount(totpInfo) + loadAccounts() + showManualAdd.value = false + Toast.makeText(this@MainActivity, "账户添加成功", Toast.LENGTH_SHORT).show() + } + ) + } + + // 账户详情对话框 + AnimatedVisibility( + visible = showAccountDetails.value, + enter = fadeIn() + scaleIn( + initialScale = 0.8f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ), + exit = fadeOut() + scaleOut(targetScale = 0.8f) + ) { + selectedAccount.value?.let { account -> + AccountDetailsDialog( + account = account, + onDismiss = { showAccountDetails.value = false }, + onEdit = { newServiceName, newUsername -> + // 更新账户信息 + localAccountManager.updateAccount(account, newServiceName, newUsername) + loadAccounts() + log("✅ 账户编辑成功: ${account.name} -> $newServiceName") + Toast.makeText(this@MainActivity, "账户编辑成功", Toast.LENGTH_SHORT).show() + } + ) + } + } + } + } + + @Composable + fun EmptyState() { + // 浮动动画状态 + var isVisible by remember { mutableStateOf(false) } + + // 图标脉冲动画 + val pulseScale by animateFloatAsState( + targetValue = if (isVisible) 1.05f else 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 2000, easing = EaseInOutSine), + repeatMode = RepeatMode.Reverse + ), + label = "pulse_scale" + ) + + // 启动动画 + LaunchedEffect(Unit) { + delay(200) + isVisible = true + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // 主图标 - 带脉冲和滑入动画 + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ) + fadeIn(animationSpec = tween(800)) + ) { + Icon( + imageVector = Icons.Default.Security, + contentDescription = null, + modifier = Modifier + .size(120.dp) + .graphicsLayer( + scaleX = pulseScale, + scaleY = pulseScale + ), + tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 标题文字 - 从下方滑入 + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { it / 2 }, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ) + fadeIn(animationSpec = tween(600)) + ) { + Text( + text = "还没有账户", + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 描述文字 - 最后出现 + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { it / 2 }, + animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy) + ) + fadeIn(animationSpec = tween(800)) + ) { + Text( + text = "点击右下角的 + 按钮开始添加您的第一个验证器账户", + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + } + } + + @OptIn(ExperimentalAnimationApi::class) + @Composable + fun AccountsList( + accounts: List, + isReorderMode: Boolean = false, + onReorder: (List) -> Unit = {} + ) { + var reorderableAccounts by remember { mutableStateOf(accounts) } + + // 滑动状态管理 - 跟踪当前滑动的账户 + var currentSwipedAccount by remember { mutableStateOf(null) } + + // 动画状态 - 用于列表项的错开入场动画 + var listVisible by remember { mutableStateOf(false) } + + // 当accounts变化时更新reorderableAccounts + LaunchedEffect(accounts) { + reorderableAccounts = accounts + // 重置列表可见性以触发重新入场动画 + if (accounts != reorderableAccounts) { + listVisible = false + delay(50) + listVisible = true + } + } + + // 启动列表入场动画 + LaunchedEffect(Unit) { + delay(100) // 稍微延迟,让主界面先出现 + listVisible = true + } + + val reorderableState = rememberReorderableLazyListState( + onMove = { from, to -> + reorderableAccounts = reorderableAccounts.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + }, + onDragEnd = { _, _ -> + onReorder(reorderableAccounts) + } + ) + + LazyColumn( + state = reorderableState.listState, + modifier = Modifier + .fillMaxSize() + .reorderable(reorderableState), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + itemsIndexed(reorderableAccounts, key = { _, account -> account.key }) { index, account -> + // 为每个列表项添加错开的入场动画 + AnimatedVisibility( + visible = listVisible, + enter = slideInVertically( + initialOffsetY = { it / 2 }, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ) + fadeIn(animationSpec = tween(durationMillis = 400)), + exit = slideOutVertically( + targetOffsetY = { -it / 2 }, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ) + fadeOut(animationSpec = tween(durationMillis = 200)) + ) { + ReorderableItem(reorderableState, key = account.key) { isDragging -> + val elevation = animateDpAsState( + targetValue = if (isDragging) 12.dp else 2.dp, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "elevation" + ) + + // 拖拽时的缩放动画 - 与手环排序保持一致 + val dragScale by animateFloatAsState( + targetValue = if (isDragging) 1.03f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "drag_scale" + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer( + scaleX = dragScale, + scaleY = dragScale + ) + .animateContentSize(), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = CardDefaults.cardColors( + containerColor = if (isDragging) + MaterialTheme.colorScheme.surfaceContainerHighest + else + MaterialTheme.colorScheme.surface + ) + ) { + Row { + // 拖拽排序模式下的账户卡片 + if (isReorderMode) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + // 拖拽手柄 - 带动画 + AnimatedVisibility( + visible = true, + enter = slideInHorizontally( + initialOffsetX = { -it }, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ) + fadeIn() + ) { + Icon( + imageVector = Icons.Default.DragHandle, + contentDescription = "拖拽排序", + modifier = Modifier + .padding(start = 16.dp) + .detectReorderAfterLongPress(reorderableState) + .animateContentSize(), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + ModernTOTPCard( + account = account, + modifier = Modifier.weight(1f), + onDelete = null, // 排序模式下禁用删除 + onViewDetails = null, // 排序模式下禁用详情 + currentSwipedAccount = null, // 排序模式下禁用滑动 + onSwipeStateChange = { _, _ -> } // 排序模式下禁用滑动 + ) + } + } else { + // 普通模式的账户卡片 - 支持二级左滑 + ModernTOTPCard( + account = account, + onDelete = { + localAccountManager.removeAccount(account) + loadAccounts() + // 删除后重置滑动状态 + currentSwipedAccount = null + }, + onViewDetails = { + selectedAccount.value = account + showAccountDetails.value = true + // 查看详情后重置滑动状态 + currentSwipedAccount = null + }, + currentSwipedAccount = currentSwipedAccount, + onSwipeStateChange = { swipedAccount, swipeState -> + currentSwipedAccount = swipedAccount + } + ) + } + } + } + } + } + } + + // 底部间距,避免被FAB遮挡 - 带动画 + item { + AnimatedVisibility( + visible = listVisible, + enter = fadeIn(animationSpec = tween(durationMillis = 300)) + ) { + Spacer(modifier = Modifier.height(88.dp)) + } + } + } + } + + @Composable + fun FloatingActionButtons( + modifier: Modifier = Modifier, + onAddClick: () -> Unit, + onSyncClick: () -> Unit + ) { + // 按钮缩放状态 + var addButtonPressed by remember { mutableStateOf(false) } + var syncButtonPressed by remember { mutableStateOf(false) } + + // 协程scope + val coroutineScope = rememberCoroutineScope() + + // 按钮缩放动画 + val addButtonScale by animateFloatAsState( + targetValue = if (addButtonPressed) 0.95f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "add_button_scale" + ) + + val syncButtonScale by animateFloatAsState( + targetValue = if (syncButtonPressed) 0.95f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "sync_button_scale" + ) + + Column( + modifier = modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.End + ) { + // 同步按钮 + FloatingActionButton( + onClick = { + syncButtonPressed = true + // 延迟执行点击,让动画效果更明显 + coroutineScope.launch { + delay(50) + syncButtonPressed = false + onSyncClick() + } + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + shape = CircleShape, + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp + ), + modifier = Modifier + .graphicsLayer( + scaleX = syncButtonScale, + scaleY = syncButtonScale + ) + .animateContentSize() + ) { + // 图标旋转动画 + var rotationAngle by remember { mutableStateOf(0f) } + val rotation by animateFloatAsState( + targetValue = rotationAngle, + animationSpec = tween(durationMillis = 300), + label = "sync_rotation" + ) + + LaunchedEffect(syncButtonPressed) { + if (syncButtonPressed) { + rotationAngle += 180f + } + } + + Icon( + imageVector = Icons.Default.Sync, + contentDescription = "同步", + modifier = Modifier.graphicsLayer(rotationZ = rotation) + ) + } + + // 添加按钮 - 主要按钮 + FloatingActionButton( + onClick = { + addButtonPressed = true + coroutineScope.launch { + delay(50) + addButtonPressed = false + onAddClick() + } + }, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + shape = CircleShape, + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp + ), + modifier = Modifier + .graphicsLayer( + scaleX = addButtonScale, + scaleY = addButtonScale + ) + .animateContentSize() + ) { + // 简化图标动画 - 移除旋转,只保留缩放 + Icon( + imageVector = Icons.Default.Add, + contentDescription = "添加" + ) + } + } + } + + private fun handleFileImport(uri: Uri) { + try { + val contentResolver = contentResolver + val importedAccounts = mutableListOf() + + log("📁 开始解析导入文件...") + contentResolver.openInputStream(uri)?.use { inputStream -> + BufferedReader(InputStreamReader(inputStream)).use { reader -> + val content = reader.readText() + log("📄 文件内容长度: ${content.length} 字符") + + // 智能提取所有otpauth URI + val extractedUris = extractOtpAuthUris(content) + log("🔍 找到 ${extractedUris.size} 个潜在的TOTP URI") + + extractedUris.forEachIndexed { index, uriString -> + try { + log("🔄 处理第 ${index + 1} 个URI...") + log("📝 URI内容: ${uriString.take(100)}...") + + // 尝试解析为Steam JSON格式 + val steamAccount = parseSteamJson(uriString) + if (steamAccount != null) { + log("🎮 识别为Steam账户") + + // 检查是否已存在相同账户(避免重复添加) + val existingAccount = importedAccounts.find { + it.name == steamAccount.name && it.key == steamAccount.key + } + if (existingAccount == null) { + importedAccounts.add(steamAccount) + log("✅ 成功添加Steam账户: ${steamAccount.name}") + } else { + log("⚠️ Steam账户已存在,跳过: ${steamAccount.name}") + } + } else { + // 尝试解析为标准TOTP URL格式 + val totpAccount = parseOtpAuthUrl(uriString) + if (totpAccount != null) { + log("🔐 识别为TOTP账户") + + // 检查是否已存在相同账户 + val existingAccount = importedAccounts.find { + it.name == totpAccount.name && it.key == totpAccount.key + } + if (existingAccount == null) { + importedAccounts.add(totpAccount) + log("✅ 成功添加TOTP账户: ${totpAccount.name}") + } else { + log("⚠️ TOTP账户已存在,跳过: ${totpAccount.name}") + } + } else { + log("❌ 无法解析URI: ${uriString.take(50)}...") + } + } + } catch (e: Exception) { + log("❌ 解析第 ${index + 1} 个URI时出错: ${e.message}") + } + } + } + } + + log("📊 解析完成,共找到 ${importedAccounts.size} 个有效账户") + + if (importedAccounts.isNotEmpty()) { + try { + // 保存到本地 + log("💾 开始保存账户到本地...") + var savedCount = 0 + importedAccounts.forEach { account -> + try { + localAccountManager.addAccount(account) + savedCount++ + log("✅ 账户保存成功: ${account.name} (${savedCount}/${importedAccounts.size})") + } catch (e: Exception) { + log("❌ 账户保存失败: ${account.name} - ${e.message}") + } + } + + log("🔄 重新加载账户列表...") + loadAccounts() + + log("📊 导入完成: 成功保存 ${savedCount}/${importedAccounts.size} 个账户") + Toast.makeText(this, "成功导入 ${savedCount} 个账户", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + log("❌ 保存账户时发生错误: ${e.message}") + Toast.makeText(this, "保存账户时发生错误: ${e.message}", Toast.LENGTH_SHORT).show() + } + } else { + log("❌ 文件中未找到有效的TOTP账户") + Toast.makeText(this, "文件中未找到有效的TOTP账户", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + log("❌ 文件导入失败: ${e.message}") + log("📍 错误堆栈: ${e.stackTraceToString()}") + Toast.makeText(this, "文件导入失败: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + /** + * 智能提取文本中所有的otpauth URI + * 支持各种编码格式和嵌套在JSON中的URI + */ + private fun extractOtpAuthUris(content: String): List { + val uris = mutableSetOf() // 使用Set避免重复 + + try { + log("🔍 开始智能URI提取...") + + // 优先检查是否为完整的Steam JSON格式 + if (content.trim().startsWith("{") && content.trim().endsWith("}")) { + try { + log("📄 检测到JSON格式,验证是否为Steam JSON...") + val jsonObject = JSONObject(content.trim()) + if (jsonObject.has("shared_secret") && jsonObject.has("account_name")) { + // 这是一个完整的Steam JSON,直接添加原始内容 + uris.add(content.trim()) + log("🎮 识别为完整Steam JSON格式,跳过其他URI提取方式") + return uris.toList() // 直接返回,避免重复识别 + } else { + log("⚠️ JSON格式但非Steam格式,继续其他提取方式") + } + } catch (e: Exception) { + // 不是有效的JSON,继续其他方式解析 + log("⚠️ JSON解析失败,尝试其他格式: ${e.message}") + } + } + + log("🔗 开始提取标准otpauth URI...") + + // 1. 直接查找otpauth://开头的完整URI(更宽松的匹配) + val directUriPattern = Regex("otpauth://totp/[^\\s\\n\\r\"'{}\\[\\]]+", RegexOption.IGNORE_CASE) + directUriPattern.findAll(content).forEach { match -> + val rawUri = match.value + val cleanUri = cleanAndDecodeUri(rawUri) + if (cleanUri.isNotEmpty() && isValidOtpAuthUri(cleanUri)) { + uris.add(cleanUri) + log("🔗 直接找到URI: ${cleanUri.take(50)}...") + } + } + + // 2. 查找JSON中的uri字段(只有在不是Steam JSON的情况下) + val jsonUriPattern = Regex("\"uri\"\\s*:\\s*\"([^\"]+)\"", RegexOption.IGNORE_CASE) + jsonUriPattern.findAll(content).forEach { match -> + val rawUri = match.groupValues[1] + val cleanUri = cleanAndDecodeUri(rawUri) + if (cleanUri.startsWith("otpauth://totp/", ignoreCase = true) && isValidOtpAuthUri(cleanUri)) { + uris.add(cleanUri) + log("🔗 JSON中找到URI: ${cleanUri.take(50)}...") + } + } + + // 3. 查找引号包围的otpauth URI + val quotedUriPattern = Regex("[\"']otpauth://totp/[^\"']+[\"']", RegexOption.IGNORE_CASE) + quotedUriPattern.findAll(content).forEach { match -> + val rawUri = match.value.trim('"', '\'') + val cleanUri = cleanAndDecodeUri(rawUri) + if (cleanUri.isNotEmpty() && isValidOtpAuthUri(cleanUri)) { + uris.add(cleanUri) + log("🔗 引号中找到URI: ${cleanUri.take(50)}...") + } + } + + } catch (e: Exception) { + log("❌ 提取URI时出错: ${e.message}") + } + + log("🔍 URI提取完成,总共找到 ${uris.size} 个不重复的有效URI") + return uris.toList() + } + + /** + * 验证是否为有效的otpauth URI + */ + private fun isValidOtpAuthUri(uri: String): Boolean { + return try { + val parsedUri = Uri.parse(uri) + parsedUri.scheme?.equals("otpauth", ignoreCase = true) == true && + parsedUri.host?.equals("totp", ignoreCase = true) == true && + !parsedUri.getQueryParameter("secret").isNullOrEmpty() + } catch (e: Exception) { + false + } + } + + /** + * 清理和解码URI,处理各种编码格式 + */ + private fun cleanAndDecodeUri(rawUri: String): String { + return try { + var cleanUri = rawUri.trim() + + // 处理Unicode转义序列 (如 \u0026) + cleanUri = cleanUri.replace("\\u0026", "&") + .replace("\\u003D", "=") + .replace("\\u003F", "?") + .replace("\\u0023", "#") + .replace("\\u002F", "/") + .replace("\\u003A", ":") + + // 处理URL编码 + cleanUri = java.net.URLDecoder.decode(cleanUri, "UTF-8") + + // 移除可能的引号 + cleanUri = cleanUri.removePrefix("\"").removeSuffix("\"") + .removePrefix("'").removeSuffix("'") + + // 验证是否为有效的otpauth URI + if (cleanUri.startsWith("otpauth://", ignoreCase = true)) { + cleanUri + } else { + "" + } + } catch (e: Exception) { + log("⚠️ URI解码失败: ${e.message}") + rawUri // 返回原始URI作为后备 + } + } + + private fun parseSteamJson(jsonString: String): TOTPInfo? { + try { + log("🎮 开始解析Steam JSON...") + + if (!jsonString.startsWith("{") || !jsonString.endsWith("}")) { + log("⚠️ 不是JSON格式,跳过Steam解析") + return null // 不是JSON格式 + } + + log("📄 尝试解析JSON对象...") + val jsonObject = JSONObject(jsonString) + log("✅ JSON解析成功") + + // 检查是否包含Steam相关字段 + if (!jsonObject.has("shared_secret") || !jsonObject.has("account_name")) { + log("⚠️ 缺少Steam必需字段 (shared_secret 或 account_name),跳过") + return null + } + + val accountName = jsonObject.getString("account_name") + log("📝 账户名: ${accountName}") + + // 🔄 关键修复:优先从URI字段中提取Base32编码的secret + var finalSecret: String? = null + var serviceName = "Steam" + var userName = accountName + + if (jsonObject.has("uri")) { + log("🔗 找到URI字段,从中提取secret...") + val uri = jsonObject.getString("uri") + log("📝 URI内容: $uri") + + try { + val parsedUri = Uri.parse(uri) + + // 从URI中提取secret参数(Base32编码) + val secretFromUri = parsedUri.getQueryParameter("secret") + if (!secretFromUri.isNullOrEmpty()) { + finalSecret = secretFromUri + log("🔑 从URI提取到secret: ${secretFromUri.take(8)}...") + } + + // 从URI中提取服务名和用户名 + val path = parsedUri.path?.removePrefix("/") + if (path != null && path.contains(":")) { + val parts = path.split(":", limit = 2) + serviceName = parts[0].trim() + userName = parts[1].trim() + log("📝 从URI解析到服务名: ${serviceName}, 用户名: ${userName}") + } + + } catch (e: Exception) { + log("⚠️ URI解析失败: ${e.message}") + } + } + + // 如果URI中没有找到secret,才使用shared_secret作为后备 + if (finalSecret.isNullOrEmpty()) { + log("⚠️ URI中未找到secret,尝试使用shared_secret作为后备") + val sharedSecret = jsonObject.getString("shared_secret") + + // 检查shared_secret是否看起来像Base64(包含+/=字符) + if (sharedSecret.contains("+") || sharedSecret.contains("/") || sharedSecret.contains("=")) { + log("🔐 shared_secret似乎是Base64格式,但Steam通常使用Base32 secret") + // 在这种情况下,我们可能需要特殊处理,但通常Steam JSON应该包含URI + return null + } else { + finalSecret = sharedSecret + log("🔑 使用shared_secret作为secret: ${sharedSecret.take(8)}...") + } + } + + // 验证最终的secret + if (finalSecret.isNullOrBlank()) { + log("❌ 未找到有效的secret,无法创建Steam账户") + return null + } + + log("📝 最终解析结果: 服务=${serviceName}, 用户=${userName}, secret长度=${finalSecret.length}") + + val totpInfo = TOTPInfo( + name = serviceName, + usr = userName, + key = finalSecret, // 使用从URI中提取的Base32编码secret + algorithm = "SHA1", // Steam使用SHA1 + digits = 5, // Steam使用5位验证码 + period = 30, // Steam使用30秒周期 + type = "steam" // 标记为Steam类型 + ) + + log("✅ Steam账户创建成功") + return totpInfo + + } catch (e: Exception) { + log("❌ 解析Steam JSON失败: ${e.message}") + log("📍 Steam JSON解析错误堆栈: ${e.stackTraceToString()}") + return null + } + } + + private fun handleQRCode(qrCode: String) { + try { + val totpInfo = parseOtpAuthUrl(qrCode) + if (totpInfo != null) { + localAccountManager.addAccount(totpInfo) + loadAccounts() + log("✅ 扫码添加账户: ${totpInfo.name}") + Toast.makeText(this, "账户添加成功", Toast.LENGTH_SHORT).show() + } else { + log("❌ 无效的二维码") + Toast.makeText(this, "无效的二维码", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + log("❌ 解析二维码失败: ${e.message}") + Toast.makeText(this, "解析二维码失败: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun parseOtpAuthUrl(url: String): TOTPInfo? { + if (!url.startsWith("otpauth://totp/", ignoreCase = true)) return null + + try { + val uri = Uri.parse(url) + val path = uri.path?.removePrefix("/") ?: return null + val secret = uri.getQueryParameter("secret") ?: return null + + // 获取issuer参数(服务提供商) + val issuer = uri.getQueryParameter("issuer")?.trim() + + log("🔍 解析URI: path=$path, issuer=$issuer") + + // 解析服务名和用户名的逻辑 + val (serviceName, userName) = when { + // 情况1:path包含冒号,格式为 "服务名:用户名" + path.contains(":") -> { + val parts = path.split(":", limit = 2) + val pathService = parts[0].trim() + val pathUser = if (parts.size > 1) parts[1].trim() else "" + + // 如果有issuer参数,优先使用issuer作为服务名 + val finalService = if (!issuer.isNullOrEmpty()) issuer else pathService + Pair(finalService, pathUser) + } + + // 情况2:path不包含冒号,但有issuer参数 + !issuer.isNullOrEmpty() -> { + // path作为用户名,issuer作为服务名 + Pair(issuer, path.trim()) + } + + // 情况3:path不包含冒号,也没有issuer + else -> { + // path作为服务名,用户名为空 + Pair(path.trim(), "") + } + } + + // 后处理:避免重复信息 + val finalUserName = when { + // 如果服务名和用户名完全相同,清空用户名 + serviceName.equals(userName, ignoreCase = true) -> "" + // 如果用户名为空,保持为空 + userName.isEmpty() -> "" + else -> userName + } + + // 处理空服务名的情况 + val finalServiceName = if (serviceName.isEmpty()) "Unknown Service" else serviceName + + // 解析其他参数 + val algorithm = uri.getQueryParameter("algorithm") ?: "SHA1" + val digits = uri.getQueryParameter("digits")?.toIntOrNull() ?: 6 + val period = uri.getQueryParameter("period")?.toIntOrNull() ?: 30 + + log("📝 解析结果: 服务=${finalServiceName}, 用户=${finalUserName}, 算法=${algorithm}, 位数=${digits}") + + return TOTPInfo( + name = finalServiceName, + usr = finalUserName, + key = secret, + algorithm = algorithm, + digits = digits, + period = period, + type = if (finalServiceName.equals("Steam", ignoreCase = true)) "steam" else "totp" + ) + } catch (e: Exception) { + log("❌ 解析TOTP URL失败: ${e.message}") + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/AddComponents.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/AddComponents.kt new file mode 100644 index 0000000..f97807e --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/AddComponents.kt @@ -0,0 +1,353 @@ +package cn.deepfal.band.TOTPauthenticator.components + +import androidx.compose.animation.* +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import cn.deepfal.band.TOTPauthenticator.data.TOTPInfo + +@Composable +fun AddTokenMenu( + onDismiss: () -> Unit, + onScanQR: () -> Unit, + onImportFile: () -> Unit, + onManualAdd: () -> Unit +) { + Box(modifier = Modifier.fillMaxSize()) { + // 背景遮罩 - 简洁淡入 + Box( + modifier = Modifier + .fillMaxSize() + .background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.5f)) + .clickable { onDismiss() } + ) + + // 菜单内容 - 不再重复动画,由外层控制 + Card( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + .clickable(enabled = false) { }, // 阻止点击传播 + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) // 移除阴影 + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + // 标题 + Text( + text = "添加账户", + fontSize = 22.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 24.dp) + ) + + // 菜单选项 - 简洁展示,无额外动画 + AddMenuOption( + icon = Icons.Default.QrCodeScanner, + title = "扫描二维码", + subtitle = "扫描服务提供的二维码", + onClick = onScanQR + ) + + Spacer(modifier = Modifier.height(12.dp)) + + AddMenuOption( + icon = Icons.Default.FileOpen, + title = "导入文件", + subtitle = "从备份文件中导入账户", + onClick = onImportFile + ) + + Spacer(modifier = Modifier.height(12.dp)) + + AddMenuOption( + icon = Icons.Default.Edit, + title = "手动输入", + subtitle = "手动输入账户信息", + onClick = onManualAdd + ) + + Spacer(modifier = Modifier.height(20.dp)) + + // 取消按钮 + TextButton( + onClick = onDismiss, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "取消", + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} + +@Composable +fun AddMenuOption( + icon: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { onClick() }, + color = MaterialTheme.colorScheme.surfaceContainerLow + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = title, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = subtitle, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + ) + } + } +} + +@Composable +fun ManualAddDialog( + onDismiss: () -> Unit, + onAdd: (TOTPInfo) -> Unit +) { + var serviceName by remember { mutableStateOf("") } + var accountName by remember { mutableStateOf("") } + var secretKey by remember { mutableStateOf("") } + var selectedType by remember { mutableStateOf("totp") } + var algorithm by remember { mutableStateOf("SHA1") } + var digits by remember { mutableStateOf("6") } + var period by remember { mutableStateOf("30") } + + Dialog(onDismissRequest = onDismiss) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + // 标题 + Text( + text = "手动添加账户", + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 24.dp) + ) + + // 服务名称 + OutlinedTextField( + value = serviceName, + onValueChange = { serviceName = it }, + label = { Text("服务名称") }, + placeholder = { Text("例如: Google") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // 账户名称 + OutlinedTextField( + value = accountName, + onValueChange = { accountName = it }, + label = { Text("账户名称") }, + placeholder = { Text("例如: user@example.com") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // 密钥 + OutlinedTextField( + value = secretKey, + onValueChange = { secretKey = it }, + label = { Text("密钥") }, + placeholder = { Text("Base32格式的密钥") }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // 类型选择 + Text( + text = "类型", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilterChip( + onClick = { selectedType = "totp" }, + label = { Text("TOTP") }, + selected = selectedType == "totp" + ) + FilterChip( + onClick = { selectedType = "steam" }, + label = { Text("Steam") }, + selected = selectedType == "steam" + ) + } + + // 高级选项(仅TOTP) + if (selectedType == "totp") { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "高级选项", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // 位数 + OutlinedTextField( + value = digits, + onValueChange = { digits = it }, + label = { Text("位数") }, + modifier = Modifier.weight(1f), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + + // 周期 + OutlinedTextField( + value = period, + onValueChange = { period = it }, + label = { Text("周期(秒)") }, + modifier = Modifier.weight(1f), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 算法选择 + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilterChip( + onClick = { algorithm = "SHA1" }, + label = { Text("SHA1") }, + selected = algorithm == "SHA1" + ) + FilterChip( + onClick = { algorithm = "SHA256" }, + label = { Text("SHA256") }, + selected = algorithm == "SHA256" + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 按钮 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onDismiss) { + Text("取消") + } + + Spacer(modifier = Modifier.width(8.dp)) + + Button( + onClick = { + if (serviceName.isNotBlank() && secretKey.isNotBlank()) { + val totpInfo = TOTPInfo( + name = serviceName.trim(), + usr = accountName.trim(), + key = secretKey.trim(), + algorithm = if (selectedType == "steam") "SHA1" else algorithm, + digits = if (selectedType == "steam") 5 else (digits.toIntOrNull() ?: 6), + period = if (selectedType == "steam") 30 else (period.toIntOrNull() ?: 30), + type = selectedType + ) + onAdd(totpInfo) + } + }, + enabled = serviceName.isNotBlank() && secretKey.isNotBlank() + ) { + Text("添加") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ModernTOTPCard.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ModernTOTPCard.kt new file mode 100644 index 0000000..f5a1f8d --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ModernTOTPCard.kt @@ -0,0 +1,841 @@ +package cn.deepfal.band.TOTPauthenticator.components + +import androidx.compose.animation.* +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cn.deepfal.band.TOTPauthenticator.data.TOTPInfo +import kotlinx.coroutines.delay +import kotlin.math.roundToInt + +// 滑动状态枚举 +enum class SwipeState { + NONE, // 未滑动 + DETAILS, // 第一级:显示详情按钮 + DELETE // 第二级:显示删除按钮 +} + +@Composable +fun ModernTOTPCard( + account: TOTPInfo, + modifier: Modifier = Modifier, + onDelete: (() -> Unit)? = null, + onViewDetails: (() -> Unit)? = null, + currentSwipedAccount: TOTPInfo? = null, + onSwipeStateChange: (TOTPInfo?, SwipeState) -> Unit = { _, _ -> } +) { + // 实时更新验证码 + var currentCode by remember { mutableStateOf(account.generateCurrentCode()) } + var remainingSeconds by remember { mutableStateOf(account.getRemainingSeconds()) } + var progress by remember { mutableStateOf(account.getProgress()) } + + // 滑动状态 + var offsetX by remember { mutableStateOf(0f) } + var swipeState by remember { mutableStateOf(SwipeState.NONE) } + + // 删除确认对话框状态 + var showDeleteConfirmation by remember { mutableStateOf(false) } + + // 验证码更新动画状态 + var codeUpdateTrigger by remember { mutableStateOf(false) } + + // 复制状态和动画 + var showCopySuccess by remember { mutableStateOf(false) } + var copyAnimationTrigger by remember { mutableStateOf(false) } + + // 获取剪贴板管理器和上下文 + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + + val density = LocalDensity.current + val detailsOffset = with(density) { 80.dp.toPx() } + val deleteOffset = with(density) { 160.dp.toPx() } + + // 卡片入场动画 + var isVisible by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + delay(50) // 错开动画时间 + isVisible = true + } + + // 验证码更新时的脉冲效果 + val codeScale by animateFloatAsState( + targetValue = if (codeUpdateTrigger) 1.05f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "code_pulse" + ) + + // 复制动画效果 - 简化 + val copyScale by animateFloatAsState( + targetValue = if (copyAnimationTrigger) 1.1f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "copy_scale" + ) + + // 复制功能 + fun copyCodeToClipboard() { + clipboardManager.setText(AnnotatedString(currentCode)) + copyAnimationTrigger = true + showCopySuccess = true + } + + // 复制动画重置 + LaunchedEffect(copyAnimationTrigger) { + if (copyAnimationTrigger) { + delay(150) + copyAnimationTrigger = false + } + } + + // 复制成功提示自动隐藏 + LaunchedEffect(showCopySuccess) { + if (showCopySuccess) { + delay(1500) + showCopySuccess = false + } + } + + // 监听当前滑动账户变化 + LaunchedEffect(currentSwipedAccount) { + if (currentSwipedAccount != null && currentSwipedAccount != account) { + offsetX = 0f + swipeState = SwipeState.NONE + } + } + + // 每秒更新验证码 - 带动画效果 + LaunchedEffect(Unit) { + while (true) { + val newCode = account.generateCurrentCode() + if (newCode != currentCode) { + // 触发验证码更新动画 + codeUpdateTrigger = true + delay(100) + currentCode = newCode + codeUpdateTrigger = false + } + remainingSeconds = account.getRemainingSeconds() + progress = account.getProgress() + delay(1000) + } + } + + // 紧急状态的颜色 - 移除闪烁效果,保持平稳显示 + val urgentColorAlpha by animateFloatAsState( + targetValue = if (remainingSeconds <= 5) 0.15f else 0f, + animationSpec = tween(durationMillis = 300, easing = EaseOutCubic), + label = "urgent_background" + ) + + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { it / 2 }, + animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy) + ) + fadeIn(animationSpec = tween(400)), + exit = slideOutVertically( + targetOffsetY = { -it / 2 }, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) + ) + fadeOut(animationSpec = tween(200)) + ) { + Box( + modifier = modifier.fillMaxWidth() + ) { + // 背景层:操作按钮 + Row( + modifier = Modifier + .fillMaxWidth() + .height(80.dp) + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // 详情按钮 + AnimatedVisibility( + visible = swipeState == SwipeState.DETAILS || swipeState == SwipeState.DELETE, + enter = fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + + scaleIn(animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)), + exit = fadeOut(animationSpec = tween(200)) + scaleOut(animationSpec = tween(200)) + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer) + .clickable { + onViewDetails?.invoke() + offsetX = 0f + swipeState = SwipeState.NONE + onSwipeStateChange(null, SwipeState.NONE) + } + .animateContentSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "查看详情", + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(20.dp) + ) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + // 删除按钮 + AnimatedVisibility( + visible = swipeState == SwipeState.DELETE, + enter = fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + + scaleIn(animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)), + exit = fadeOut(animationSpec = tween(200)) + scaleOut(animationSpec = tween(200)) + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.errorContainer) + .clickable { + showDeleteConfirmation = true + } + .animateContentSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "删除", + tint = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(20.dp) + ) + } + } + } + + // 前景层:彻底修复圆角问题 - 使用Box+背景+clip的方式 + Box( + modifier = Modifier + .fillMaxWidth() + .offset { IntOffset(offsetX.roundToInt(), 0) } + .clip(RoundedCornerShape(16.dp)) + .background( + if (urgentColorAlpha > 0f) { + MaterialTheme.colorScheme.errorContainer.copy(alpha = urgentColorAlpha) + } else { + MaterialTheme.colorScheme.surface + } + ) + .draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { delta -> + val newOffset = offsetX + delta + if (newOffset <= 0) { + offsetX = newOffset.coerceAtLeast(-deleteOffset) + } + }, + onDragStopped = { velocity -> + val targetState = when { + velocity < -300 && offsetX < -20f -> { + when (swipeState) { + SwipeState.NONE -> SwipeState.DETAILS + SwipeState.DETAILS -> SwipeState.DELETE + SwipeState.DELETE -> SwipeState.DELETE + } + } + offsetX < -deleteOffset * 0.5f -> SwipeState.DELETE + offsetX < -detailsOffset * 0.3f -> SwipeState.DETAILS + else -> SwipeState.NONE + } + + when (targetState) { + SwipeState.DETAILS -> { + offsetX = -detailsOffset + swipeState = SwipeState.DETAILS + onSwipeStateChange(account, SwipeState.DETAILS) + } + SwipeState.DELETE -> { + offsetX = -deleteOffset + swipeState = SwipeState.DELETE + onSwipeStateChange(account, SwipeState.DELETE) + } + SwipeState.NONE -> { + offsetX = 0f + swipeState = SwipeState.NONE + onSwipeStateChange(null, SwipeState.NONE) + } + } + } + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 左侧:服务图标 - 带旋转动画 + ServiceIcon(serviceName = account.name) + + Spacer(modifier = Modifier.width(16.dp)) + + // 中间:账户信息 + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = account.name, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1 + ) + + if (account.usr.isNotEmpty()) { + Text( + text = account.usr, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + maxLines = 1 + ) + } + } + + Spacer(modifier = Modifier.width(16.dp)) + + // 右侧:验证码和倒计时 - 带动画效果和点击复制 + Box { + Column( + horizontalAlignment = Alignment.End, + modifier = Modifier + .animateContentSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + copyCodeToClipboard() + } + .padding(8.dp) + ) { + // 验证码 - 带缩放和复制动画 + Text( + text = if (account.type.lowercase() == "steam") { + currentCode + } else { + "${currentCode.take(3)} ${currentCode.drop(3)}" + }, + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = FontFamily.Monospace, + color = getCodeColor(remainingSeconds), + letterSpacing = 1.sp, + modifier = Modifier.graphicsLayer( + scaleX = codeScale * copyScale, + scaleY = codeScale * copyScale + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // 倒计时指示器 - 带动画 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "${remainingSeconds}s", + fontSize = 12.sp, + color = getCodeColor(remainingSeconds).copy(alpha = 0.7f), + fontWeight = FontWeight.Medium + ) + + // 脉冲指示器 + Box( + modifier = Modifier + .size(6.dp) + .clip(CircleShape) + .background( + if (remainingSeconds <= 10) { + getCodeColor(remainingSeconds).copy(alpha = 0.8f) + } else { + MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + } + ) + .animateContentSize() + ) + } + } + + // 简化的复制成功提示 + if (showCopySuccess) { + Surface( + modifier = Modifier + .align(Alignment.CenterEnd) + .offset(y = 20.dp), + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(6.dp) + ) { + Text( + text = "✓ 已复制", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + } + } + } + } + } + + // 删除确认对话框 - 带动画 + if (showDeleteConfirmation) { + AlertDialog( + onDismissRequest = { + showDeleteConfirmation = false + }, + title = { + Text( + text = "确认删除", + fontWeight = FontWeight.Medium + ) + }, + text = { + Column { + Text( + text = "确定要删除账户 \"${account.name}\" 吗?", + color = MaterialTheme.colorScheme.onSurface + ) + if (account.usr.isNotEmpty()) { + Text( + text = "用户:${account.usr}", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + modifier = Modifier.padding(top = 4.dp) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "此操作不可撤销。", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Medium + ) + } + }, + confirmButton = { + TextButton( + onClick = { + showDeleteConfirmation = false + onDelete?.invoke() + // 重置滑动状态 + offsetX = 0f + swipeState = SwipeState.NONE + onSwipeStateChange(null, SwipeState.NONE) + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text( + text = "删除", + fontWeight = FontWeight.Medium + ) + } + }, + dismissButton = { + TextButton( + onClick = { + showDeleteConfirmation = false + } + ) { + Text("取消") + } + } + ) + } +} + +@Composable +fun ServiceIcon(serviceName: String) { + val icon = when (serviceName.lowercase()) { + "google" -> Icons.Default.AccountCircle + "github" -> Icons.Default.Code + "microsoft" -> Icons.Default.Business + "apple" -> Icons.Default.Phone + "steam" -> Icons.Default.Games + "discord" -> Icons.Default.Chat + "twitter", "x" -> Icons.Default.Share + "facebook" -> Icons.Default.Person + "amazon" -> Icons.Default.ShoppingCart + "dropbox" -> Icons.Default.Cloud + "linkedin" -> Icons.Default.Work + else -> Icons.Default.Security + } + + val color = when (serviceName.lowercase()) { + "google" -> androidx.compose.ui.graphics.Color(0xFF4285F4) + "github" -> androidx.compose.ui.graphics.Color(0xFF181717) + "microsoft" -> androidx.compose.ui.graphics.Color(0xFF00A1F1) + "apple" -> androidx.compose.ui.graphics.Color(0xFF000000) + "steam" -> androidx.compose.ui.graphics.Color(0xFF171A21) + else -> MaterialTheme.colorScheme.primary + } + + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(color.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = serviceName, + tint = color, + modifier = Modifier.size(20.dp) + ) + } +} + +@Composable +fun getCodeColor(remainingSeconds: Int): androidx.compose.ui.graphics.Color { + return when { + remainingSeconds <= 5 -> MaterialTheme.colorScheme.error + remainingSeconds <= 10 -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.primary + } +} + +@Composable +fun CountdownCircle( + progress: Float, + remainingSeconds: Int +) { + val isUrgent = remainingSeconds <= 5 + val color = if (isUrgent) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + } + + Box( + modifier = Modifier.size(56.dp), + contentAlignment = Alignment.Center + ) { + // 背景圆环 + CircularProgressIndicator( + progress = { 1f }, + modifier = Modifier.size(48.dp), + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f), + strokeWidth = 4.dp + ) + + // 进度圆环 + CircularProgressIndicator( + progress = { 1f - progress }, + modifier = Modifier.size(48.dp), + color = color, + strokeWidth = 4.dp + ) + + // 剩余秒数 + Text( + text = remainingSeconds.toString(), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = color + ) + } +} + +@Composable +fun TypeChip(type: String) { + val chipColor = when (type.lowercase()) { + "steam" -> androidx.compose.ui.graphics.Color(0xFF171A21) + else -> MaterialTheme.colorScheme.secondary + } + + Surface( + shape = RoundedCornerShape(12.dp), + color = chipColor.copy(alpha = 0.1f), + modifier = Modifier.padding(0.dp) + ) { + Text( + text = type.uppercase(), + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + color = chipColor, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } +} + +@Composable +fun AccountActionDialog( + accountName: String, + onDismiss: () -> Unit, + onViewDetails: () -> Unit, + onDelete: () -> Unit +) { + var showDeleteConfirmation by remember { mutableStateOf(false) } + + if (showDeleteConfirmation) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = "确认删除") + }, + text = { + Text(text = "确定要删除账户 \"$accountName\" 吗?此操作不可撤销。") + }, + confirmButton = { + TextButton( + onClick = onDelete, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text("删除") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("取消") + } + } + ) + } else { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = accountName) + }, + text = { + Text(text = "选择要执行的操作") + }, + confirmButton = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton(onClick = onViewDetails) { + Text("查看详情") + } + TextButton( + onClick = { showDeleteConfirmation = true }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text("删除") + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("取消") + } + } + ) + } +} + +@Composable +fun AccountDetailsDialog( + account: TOTPInfo, + onDismiss: () -> Unit, + onEdit: ((String, String) -> Unit)? = null // 回调函数:(newServiceName, newUsername) -> Unit +) { + var showEditDialog by remember { mutableStateOf(false) } + + if (showEditDialog) { + EditAccountDialog( + account = account, + onDismiss = { showEditDialog = false }, + onSave = { newServiceName, newUsername -> + onEdit?.invoke(newServiceName, newUsername) + showEditDialog = false + onDismiss() + } + ) + } else { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ServiceIcon(serviceName = account.name) + Text(text = "账户详情") + } + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + DetailItem(label = "服务名称", value = account.name) + DetailItem(label = "用户名", value = account.usr) + DetailItem(label = "算法", value = account.algorithm) + DetailItem(label = "位数", value = "${account.digits} 位") + DetailItem(label = "周期", value = "${account.period} 秒") + DetailItem(label = "类型", value = account.type.uppercase()) + } + }, + confirmButton = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (onEdit != null) { + TextButton( + onClick = { showEditDialog = true }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ) + ) { + Text("编辑") + } + } + TextButton(onClick = onDismiss) { + Text("关闭") + } + } + } + ) + } +} + +@Composable +fun DetailItem(label: String, value: String) { + Column { + Text( + text = label, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + fontWeight = FontWeight.Medium + ) + Text( + text = value, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +@Composable +fun EditAccountDialog( + account: TOTPInfo, + onDismiss: () -> Unit, + onSave: (String, String) -> Unit +) { + var serviceName by remember { mutableStateOf(account.name) } + var username by remember { mutableStateOf(account.usr) } + val isValid = serviceName.trim().isNotEmpty() + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = "编辑账户") + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + // 服务名称输入框 + Column { + Text( + text = "服务名称 *", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(4.dp)) + OutlinedTextField( + value = serviceName, + onValueChange = { serviceName = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("例如:Google") }, + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) + ) + } + + // 用户名输入框 + Column { + Text( + text = "用户名", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(4.dp)) + OutlinedTextField( + value = username, + onValueChange = { username = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("例如:user@example.com") }, + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) + ) + } + + // 提示信息 + Text( + text = "* 必填字段", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(top = 4.dp) + ) + } + }, + confirmButton = { + TextButton( + onClick = { + if (isValid) { + onSave(serviceName.trim(), username.trim()) + } + }, + enabled = isValid, + colors = ButtonDefaults.textButtonColors( + contentColor = if (isValid) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.outline + ) + ) { + Text("保存") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("取消") + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ReorderableBandAccountsList.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ReorderableBandAccountsList.kt new file mode 100644 index 0000000..c3f4bbf --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/ReorderableBandAccountsList.kt @@ -0,0 +1,175 @@ +package cn.deepfal.band.TOTPauthenticator.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.burnoutcrew.reorderable.* + +@Composable +fun ReorderableBandAccountsList( + accounts: List, + onReorder: (List) -> Unit +) { + var reorderableAccounts by remember { mutableStateOf(accounts) } + + // 当accounts变化时更新reorderableAccounts + LaunchedEffect(accounts) { + reorderableAccounts = accounts + } + + val reorderableState = rememberReorderableLazyListState( + onMove = { from, to -> + reorderableAccounts = reorderableAccounts.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + }, + onDragEnd = { _, _ -> + onReorder(reorderableAccounts) + } + ) + + Column(modifier = Modifier.fillMaxSize()) { + // 重排序提示 + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "拖拽账户卡片可重新排序手环上的账户顺序", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + // 可重排序的账户列表 + LazyColumn( + state = reorderableState.listState, + modifier = Modifier + .weight(1f) + .reorderable(reorderableState), + contentPadding = PaddingValues(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(reorderableAccounts, key = { _, account -> account.key }) { index, account -> + ReorderableItem(reorderableState, key = account.key) { isDragging -> + val elevation = animateDpAsState( + targetValue = if (isDragging) 16.dp else 2.dp, + label = "elevation" + ) + + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = elevation.value), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 拖拽手柄 + Icon( + imageVector = Icons.Default.DragHandle, + contentDescription = "拖拽排序", + modifier = Modifier + .detectReorderAfterLongPress(reorderableState) + .padding(end = 12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + // 状态指示器 + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Watch, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(20.dp) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + // 账户信息 + Column(modifier = Modifier.weight(1f)) { + Text( + text = account.name, + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + if (account.usr.isNotEmpty()) { + Text( + text = account.usr, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "手环账户", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Medium + ) + } + + // 序号显示 + Text( + text = "#${index + 1}", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Medium + ) + } + } + } + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/SyncScreen.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/SyncScreen.kt new file mode 100644 index 0000000..38cf0b5 --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/components/SyncScreen.kt @@ -0,0 +1,1138 @@ +package cn.deepfal.band.TOTPauthenticator.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cn.deepfal.band.TOTPauthenticator.sync.SyncAccount +import cn.deepfal.band.TOTPauthenticator.sync.SyncStatus +import org.burnoutcrew.reorderable.* +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.Spring +import androidx.compose.animation.animateContentSize +import androidx.compose.ui.graphics.graphicsLayer +import cn.deepfal.band.TOTPauthenticator.components.ServiceIcon +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SyncScreen( + onClose: () -> Unit, + phoneAccounts: List = emptyList(), + watchAccounts: List = emptyList(), + connectionStatus: String = "未连接", + isConnected: Boolean = false, + syncLogs: List = emptyList(), + onSyncToWatch: (List) -> Unit = {}, + onSyncToPhone: (List) -> Unit = {}, + onRequestBandAccounts: () -> Unit = {}, + onReconnect: () -> Unit = {}, + onDeleteFromBand: (cn.deepfal.band.TOTPauthenticator.data.TOTPInfo) -> Unit = {}, + onReorderBandAccounts: (List) -> Unit = {} +) { + var showLogs by remember { mutableStateOf(false) } + var showSortDialog by remember { mutableStateOf(false) } + var isReorderMode by remember { mutableStateOf(false) } + + val context = LocalContext.current + + // 处理同步界面内的返回键 + DisposableEffect(Unit) { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + when { + showSortDialog -> { + showSortDialog = false + } + showLogs -> { + showLogs = false + } + isReorderMode -> { + isReorderMode = false + } + else -> { + onClose() + } + } + } + } + + (context as ComponentActivity).onBackPressedDispatcher.addCallback(callback) + + onDispose { + callback.remove() + } + } + + // 合并去重算法 - 保持手环端的原始顺序 + val mergedAccounts = remember(phoneAccounts, watchAccounts) { + val phoneMap = phoneAccounts.associateBy { "${it.name}_${it.key}" } + val watchMap = watchAccounts.associateBy { "${it.name}_${it.key}" } + val merged = mutableListOf() + + // 首先按手环返回的顺序添加手环账户(保持手环端排序) + watchAccounts.forEach { account -> + val key = "${account.name}_${account.key}" + val status = if (phoneMap.containsKey(key)) { + SyncStatus.SYNCED // 手机和手环都有 + } else { + SyncStatus.WATCH_ONLY // 仅手环有 + } + merged.add(SyncAccount(account, status)) + } + + // 然后添加仅手机账户(这些账户不在手环上,所以顺序不重要) + phoneAccounts.forEach { account -> + val key = "${account.name}_${account.key}" + if (!watchMap.containsKey(key)) { + merged.add(SyncAccount(account, SyncStatus.PHONE_ONLY)) + } + } + + // 不再进行额外排序,完全按照手环顺序 + 仅手机账户的顺序 + merged + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .statusBarsPadding() + ) { + // 顶部标题栏 + TopAppBar( + title = { + Text( + text = if (isReorderMode) "拖拽排序手环账户" else "同步管理", + fontSize = 24.sp, + fontWeight = FontWeight.Medium + ) + }, + navigationIcon = { + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "返回" + ) + } + }, + actions = { + // 重排序按钮 - 只有手环账户时才显示 + if (watchAccounts.isNotEmpty() && isConnected) { + IconButton( + onClick = { + isReorderMode = !isReorderMode + } + ) { + Icon( + imageVector = if (isReorderMode) Icons.Default.Check else Icons.Default.Reorder, + contentDescription = if (isReorderMode) "完成排序" else "重排序手环账户" + ) + } + } + + // 排序按钮 + if (!isReorderMode) { + IconButton(onClick = { showSortDialog = true }) { + Icon( + imageVector = Icons.Default.Sort, + contentDescription = "排序" + ) + } + } + + // 日志按钮 + if (!isReorderMode) { + IconButton(onClick = { showLogs = !showLogs }) { + Icon( + imageVector = Icons.Default.List, + contentDescription = "查看日志" + ) + } + } + + // 刷新按钮 + if (!isReorderMode) { + IconButton(onClick = onRequestBandAccounts) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "刷新" + ) + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) + + if (showLogs) { + // 日志界面 + LogsView( + logs = syncLogs, + onClose = { showLogs = false } + ) + } else { + // 连接状态卡片 + ConnectionStatusCard( + connectionStatus = connectionStatus, + isConnected = isConnected, + onReconnect = onReconnect + ) + + // 主要内容 + SyncContent( + syncAccounts = mergedAccounts, + onSyncToWatch = onSyncToWatch, + onSyncToPhone = onSyncToPhone, + onDeleteFromBand = onDeleteFromBand, + isReorderMode = isReorderMode, + onReorderBandAccounts = onReorderBandAccounts + ) + } + } + + // 排序选项对话框 + if (showSortDialog) { + SortOptionsDialog( + onDismiss = { showSortDialog = false }, + onSortByStatus = { + showSortDialog = false + // 已经在mergedAccounts中实现了按状态排序 + }, + onSortByName = { + showSortDialog = false + // 可以添加更多排序选项 + } + ) + } +} + +@Composable +fun LoadingState() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + strokeWidth = 4.dp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "正在连接手环...", + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } +} + +@Composable +fun DisconnectedState() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.Watch, + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "手环未连接", + fontSize = 20.sp, + fontWeight = FontWeight.Medium + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "请确保手环已连接并开启蓝牙", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } +} + +@Composable +fun SyncContent( + syncAccounts: List, + onSyncToWatch: (List) -> Unit, + onSyncToPhone: (List) -> Unit, + onDeleteFromBand: (cn.deepfal.band.TOTPauthenticator.data.TOTPInfo) -> Unit, + isReorderMode: Boolean = false, + onReorderBandAccounts: (List) -> Unit = {} +) { + Column(modifier = Modifier.fillMaxSize()) { + // 统计信息 + if (!isReorderMode) { + SyncStatistics(syncAccounts = syncAccounts) + } + + // 账户列表 + if (isReorderMode) { + // 重排序模式:只显示手环账户并支持拖拽 + val bandAccounts = syncAccounts + .filter { it.status == SyncStatus.WATCH_ONLY || it.status == SyncStatus.SYNCED } + .map { it.account } + + ReorderableBandAccountsListImpl( + accounts = bandAccounts, + onReorder = onReorderBandAccounts + ) + } else { + // 普通模式:按类别分组显示 + GroupedAccountsList( + syncAccounts = syncAccounts, + onSyncToWatch = onSyncToWatch, + onSyncToPhone = onSyncToPhone, + onDeleteFromBand = onDeleteFromBand + ) + } + } +} + +@Composable +fun GroupedAccountsList( + syncAccounts: List, + onSyncToWatch: (List) -> Unit, + onSyncToPhone: (List) -> Unit, + onDeleteFromBand: (cn.deepfal.band.TOTPauthenticator.data.TOTPInfo) -> Unit +) { + // 按照当前顺序分组账户(手环账户在前,仅手机账户在后) + val bandAccounts = syncAccounts.filter { + it.status == SyncStatus.WATCH_ONLY || it.status == SyncStatus.SYNCED + } + val phoneOnlyAccounts = syncAccounts.filter { + it.status == SyncStatus.PHONE_ONLY + } + + Column(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // 手环账户组(按手环返回顺序显示) + if (bandAccounts.isNotEmpty()) { + item { + AccountGroupHeader( + title = "手环账户", + subtitle = "按手环顺序显示,可重排序", + icon = Icons.Default.Watch, + color = MaterialTheme.colorScheme.secondary, + count = bandAccounts.size + ) + } + + items(bandAccounts) { syncAccount -> + SyncAccountCard( + syncAccount = syncAccount, + onSyncToWatch = { onSyncToWatch(listOf(syncAccount.account)) }, + onSyncToPhone = { onSyncToPhone(listOf(syncAccount.account)) }, + onDeleteFromBand = { onDeleteFromBand(syncAccount.account) }, + showGroupIndicator = true + ) + } + + if (phoneOnlyAccounts.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + + // 仅手机账户组 + if (phoneOnlyAccounts.isNotEmpty()) { + item { + AccountGroupHeader( + title = "仅手机账户", + subtitle = "需同步到手环后才能参与排序", + icon = Icons.Default.PhoneAndroid, + color = MaterialTheme.colorScheme.primary, + count = phoneOnlyAccounts.size + ) + } + + items(phoneOnlyAccounts) { syncAccount -> + SyncAccountCard( + syncAccount = syncAccount, + onSyncToWatch = { onSyncToWatch(listOf(syncAccount.account)) }, + onSyncToPhone = { onSyncToPhone(listOf(syncAccount.account)) }, + onDeleteFromBand = null, + showGroupIndicator = true + ) + } + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + } + } + + // 底部批量操作按钮 + BottomSyncActions( + syncAccounts = syncAccounts, + onSyncAllToWatch = { phoneAccounts -> + onSyncToWatch(phoneAccounts) + }, + onSyncAllToPhone = { watchAccounts -> + onSyncToPhone(watchAccounts) + } + ) + } +} + +@Composable +fun AccountGroupHeader( + title: String, + subtitle: String, + icon: ImageVector, + color: Color, + count: Int +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = color.copy(alpha = 0.1f) + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(color.copy(alpha = 0.2f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = color, + modifier = Modifier.size(24.dp) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = color + ) + Text( + text = subtitle, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + + // 数量徽章 + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(color), + contentAlignment = Alignment.Center + ) { + Text( + text = count.toString(), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.surface + ) + } + } + } +} + +@Composable +fun SyncStatistics(syncAccounts: List) { + val phoneOnly = syncAccounts.count { it.status == SyncStatus.PHONE_ONLY } + val watchOnly = syncAccounts.count { it.status == SyncStatus.WATCH_ONLY } + val synced = syncAccounts.count { it.status == SyncStatus.SYNCED } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + StatItem( + count = phoneOnly, + label = "仅手机", + color = MaterialTheme.colorScheme.primary + ) + + StatItem( + count = watchOnly, + label = "仅手环", + color = MaterialTheme.colorScheme.secondary + ) + + StatItem( + count = synced, + label = "已同步", + color = MaterialTheme.colorScheme.tertiary + ) + } + } +} + +@Composable +fun StatItem(count: Int, label: String, color: Color) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = count.toString(), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = color + ) + Text( + text = label, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } +} + +@Composable +fun SyncAccountCard( + syncAccount: SyncAccount, + onSyncToWatch: () -> Unit, + onSyncToPhone: () -> Unit, + onDeleteFromBand: (() -> Unit)? = null, + showGroupIndicator: Boolean = false +) { + if (onDeleteFromBand != null) { + // 仅手环账户支持侧滑删除 + var showDeleteConfirmation by remember { mutableStateOf(false) } + val dismissState = rememberSwipeToDismissBoxState() + + // 处理滑动事件 + LaunchedEffect(dismissState.currentValue) { + when (dismissState.currentValue) { + SwipeToDismissBoxValue.EndToStart -> { + showDeleteConfirmation = true + dismissState.snapTo(SwipeToDismissBoxValue.Settled) + } + else -> {} + } + } + + SwipeToDismissBox( + state = dismissState, + backgroundContent = { + // 背景内容 - 显示删除提示 + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.error, RoundedCornerShape(12.dp)) + .padding(horizontal = 20.dp), + contentAlignment = Alignment.CenterEnd + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.onError + ) + Text( + text = "从手环删除", + color = MaterialTheme.colorScheme.onError, + fontWeight = FontWeight.Medium + ) + } + } + } + ) { + SyncAccountCardContent( + syncAccount = syncAccount, + onSyncToWatch = onSyncToWatch, + onSyncToPhone = onSyncToPhone, + showGroupIndicator = showGroupIndicator + ) + } + + // 删除确认对话框 + if (showDeleteConfirmation) { + AlertDialog( + onDismissRequest = { showDeleteConfirmation = false }, + title = { + Text(text = "确认删除") + }, + text = { + Text(text = "确定要从手环删除账户 \"${syncAccount.account.name}\" 吗?此操作不可撤销。") + }, + confirmButton = { + TextButton( + onClick = { + showDeleteConfirmation = false + onDeleteFromBand() + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text("删除") + } + }, + dismissButton = { + TextButton(onClick = { showDeleteConfirmation = false }) { + Text("取消") + } + } + ) + } + } else { + // 普通账户卡片(无删除功能) + SyncAccountCardContent( + syncAccount = syncAccount, + onSyncToWatch = onSyncToWatch, + onSyncToPhone = onSyncToPhone, + showGroupIndicator = showGroupIndicator + ) + } +} + +@Composable +fun SyncAccountCardContent( + syncAccount: SyncAccount, + onSyncToWatch: () -> Unit, + onSyncToPhone: () -> Unit, + showGroupIndicator: Boolean = false +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 状态指示器 + StatusIndicator(status = syncAccount.status) + + Spacer(modifier = Modifier.width(12.dp)) + + // 账户信息 + Column(modifier = Modifier.weight(1f)) { + Text( + text = syncAccount.account.name, + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + if (syncAccount.account.usr.isNotEmpty()) { + Text( + text = syncAccount.account.usr, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + StatusText(status = syncAccount.status) + } + + // 同步按钮 + when (syncAccount.status) { + SyncStatus.PHONE_ONLY -> { + IconButton(onClick = onSyncToWatch) { + Icon( + imageVector = Icons.Default.East, + contentDescription = "同步到手环", + tint = MaterialTheme.colorScheme.primary + ) + } + } + SyncStatus.WATCH_ONLY -> { + IconButton(onClick = onSyncToPhone) { + Icon( + imageVector = Icons.Default.West, + contentDescription = "同步到手机", + tint = MaterialTheme.colorScheme.secondary + ) + } + } + SyncStatus.SYNCED -> { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "已同步", + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(24.dp) + ) + } + } + } + } +} + +@Composable +fun StatusIndicator(status: SyncStatus) { + val (icon, color) = when (status) { + SyncStatus.PHONE_ONLY -> Icons.Default.PhoneAndroid to MaterialTheme.colorScheme.primary + SyncStatus.WATCH_ONLY -> Icons.Default.Watch to MaterialTheme.colorScheme.secondary + SyncStatus.SYNCED -> Icons.Default.Sync to MaterialTheme.colorScheme.tertiary + } + + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(color.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = color, + modifier = Modifier.size(20.dp) + ) + } +} + +@Composable +fun StatusText(status: SyncStatus) { + val (text, color) = when (status) { + SyncStatus.PHONE_ONLY -> "仅在手机" to MaterialTheme.colorScheme.primary + SyncStatus.WATCH_ONLY -> "仅在手环" to MaterialTheme.colorScheme.secondary + SyncStatus.SYNCED -> "已同步" to MaterialTheme.colorScheme.tertiary + } + + Text( + text = text, + fontSize = 12.sp, + color = color, + fontWeight = FontWeight.Medium + ) +} + +@Composable +fun BottomSyncActions( + syncAccounts: List, + onSyncAllToWatch: (List) -> Unit, + onSyncAllToPhone: (List) -> Unit +) { + val phoneOnlyAccounts = syncAccounts.filter { it.status == SyncStatus.PHONE_ONLY }.map { it.account } + val watchOnlyAccounts = syncAccounts.filter { it.status == SyncStatus.WATCH_ONLY }.map { it.account } + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 8.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (phoneOnlyAccounts.isNotEmpty()) { + Button( + onClick = { onSyncAllToWatch(phoneOnlyAccounts) }, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.East, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("全部同步到手环") + } + } + + if (watchOnlyAccounts.isNotEmpty()) { + OutlinedButton( + onClick = { onSyncAllToPhone(watchOnlyAccounts) }, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.West, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("全部同步到手机") + } + } + } + } +} + +@Composable +fun ConnectionStatusCard( + connectionStatus: String, + isConnected: Boolean, + onReconnect: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = if (isConnected) + MaterialTheme.colorScheme.primaryContainer + else + MaterialTheme.colorScheme.errorContainer + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (isConnected) Icons.Default.Bluetooth else Icons.Default.BluetoothDisabled, + contentDescription = null, + tint = if (isConnected) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = if (isConnected) "手环已连接" else "手环未连接", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = if (isConnected) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onErrorContainer + ) + Text( + text = connectionStatus, + fontSize = 14.sp, + color = if (isConnected) + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + else + MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f) + ) + } + + if (!isConnected) { + IconButton(onClick = onReconnect) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "重新连接", + tint = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + } +} + +@Composable +fun LogsView( + logs: List, + onClose: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + // 标题栏 + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "通信日志", + fontSize = 20.sp, + fontWeight = FontWeight.Medium + ) + + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "关闭" + ) + } + } + } + + // 日志列表 + if (logs.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Default.Message, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "暂无通信日志", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + reverseLayout = true + ) { + items(logs.reversed()) { log -> + LogItem(log = log) + } + } + } + } +} + +@Composable +fun LogItem(log: String) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) + ) { + Text( + text = log, + modifier = Modifier.padding(12.dp), + fontSize = 14.sp, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +@Composable +fun SortOptionsDialog( + onDismiss: () -> Unit, + onSortByStatus: () -> Unit, + onSortByName: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = "显示顺序说明") + }, + text = { + Column { + Text( + text = "当前显示方式:按手环实际顺序", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text("• 手环账户:按手环端的实际排序显示") + Text("• 仅手机账户:显示在手环账户下方") + Text("• 重排序:点击重排序按钮可拖拽调整手环账户顺序") + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("了解") + } + } + ) +} + +@Composable +fun ReorderableBandAccountsListImpl( + accounts: List, + onReorder: (List) -> Unit +) { + var reorderableAccounts by remember { mutableStateOf(accounts) } + + // 当accounts变化时更新reorderableAccounts + LaunchedEffect(accounts) { + reorderableAccounts = accounts + } + + val reorderableState = rememberReorderableLazyListState( + onMove = { from, to -> + reorderableAccounts = reorderableAccounts.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + }, + onDragEnd = { _, _ -> + onReorder(reorderableAccounts) + } + ) + + LazyColumn( + state = reorderableState.listState, + modifier = Modifier + .fillMaxSize() + .reorderable(reorderableState), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + itemsIndexed(reorderableAccounts, key = { _, account -> account.key }) { index, account -> + ReorderableItem(reorderableState, key = account.key) { isDragging -> + val elevation = animateDpAsState( + targetValue = if (isDragging) 12.dp else 2.dp, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "elevation" + ) + + // 拖拽时的缩放动画 - 与首页保持一致 + val dragScale by animateFloatAsState( + targetValue = if (isDragging) 1.03f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "drag_scale" + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer( + scaleX = dragScale, + scaleY = dragScale + ) + .animateContentSize(), + elevation = CardDefaults.cardElevation(defaultElevation = elevation.value), + colors = CardDefaults.cardColors( + containerColor = if (isDragging) + MaterialTheme.colorScheme.surfaceContainerHighest + else + MaterialTheme.colorScheme.surface + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 拖拽手柄 + Icon( + imageVector = Icons.Default.DragHandle, + contentDescription = "拖拽排序", + modifier = Modifier + .detectReorderAfterLongPress(reorderableState) + .padding(end = 16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + // 服务图标 + ServiceIcon(serviceName = account.name) + + Spacer(modifier = Modifier.width(16.dp)) + + // 账户信息 + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = account.name, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + if (account.usr.isNotEmpty()) { + Text( + text = account.usr, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + } + + // 当前验证码 + Text( + text = if (account.type.lowercase() == "steam") { + account.generateCurrentCode() + } else { + val code = account.generateCurrentCode() + "${code.take(3)} ${code.drop(3)}" + }, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, + color = MaterialTheme.colorScheme.primary + ) + } + } + } + } + + // 底部间距 + item { + Spacer(modifier = Modifier.height(88.dp)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/data/TOTPInfo.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/data/TOTPInfo.kt new file mode 100644 index 0000000..86c7c8b --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/data/TOTPInfo.kt @@ -0,0 +1,40 @@ +package cn.deepfal.band.TOTPauthenticator.data + +import cn.deepfal.band.TOTPauthenticator.utils.TOTPGenerator +import cn.deepfal.band.TOTPauthenticator.utils.SteamTOTPGenerator + +/** + * TOTP账户信息数据类 + */ +data class TOTPInfo( + val name: String, + val usr: String, + val key: String, + val algorithm: String = "SHA1", + val digits: Int = 6, + val period: Int = 30, + val type: String = "totp" +) { + fun toJsonString(): String { + return """{"name": "$name", "usr": "$usr", "key": "$key", "algorithm": "$algorithm", "digits": $digits, "period": $period, "type": "$type"}""" + } + + // 生成当前验证码 + fun generateCurrentCode(): String { + return if (type.lowercase() == "steam") { + SteamTOTPGenerator.generateSteamCode(key) + } else { + TOTPGenerator.generateTOTP(key, algorithm, digits, period) + } + } + + // 获取剩余时间(秒) + fun getRemainingSeconds(): Int { + return TOTPGenerator.getRemainingSeconds(period) + } + + // 获取进度(0.0-1.0) + fun getProgress(): Float { + return TOTPGenerator.getProgress(period) + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/scanner/QRCodeScanner.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/scanner/QRCodeScanner.kt new file mode 100644 index 0000000..25e73d9 --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/scanner/QRCodeScanner.kt @@ -0,0 +1,370 @@ +@file:OptIn(ExperimentalPermissionsApi::class) + +package cn.deepfal.band.TOTPauthenticator.scanner + +import android.annotation.SuppressLint +import android.view.ViewGroup +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.FlashOff +import androidx.compose.material.icons.filled.FlashOn +import androidx.compose.material.icons.filled.QrCodeScanner +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun QRCodeScanner( + onCodeScanned: (String) -> Unit, + onClose: () -> Unit +) { + val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA) + + Box(modifier = Modifier.fillMaxSize()) { + when { + cameraPermissionState.status.isGranted -> { + CameraPreview( + onCodeScanned = onCodeScanned, + onClose = onClose + ) + } + else -> { + CameraPermissionRequest( + permissionState = cameraPermissionState, + onClose = onClose + ) + } + } + } +} + +@Composable +fun CameraPermissionRequest( + permissionState: PermissionState, + onClose: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black), + contentAlignment = Alignment.Center + ) { + // 关闭按钮 + IconButton( + onClick = onClose, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(16.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "关闭", + tint = Color.White + ) + } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Default.QrCodeScanner, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "需要相机权限", + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "扫描二维码需要使用相机功能", + fontSize = 14.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { permissionState.launchPermissionRequest() }, + modifier = Modifier.fillMaxWidth() + ) { + Text("授予权限") + } + } + } + } +} + +@SuppressLint("UnsafeOptInUsageError") +@Composable +fun CameraPreview( + onCodeScanned: (String) -> Unit, + onClose: () -> Unit +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var flashEnabled by remember { mutableStateOf(false) } + var camera by remember { mutableStateOf(null) } + + val preview = remember { Preview.Builder().build() } + val selector = remember { CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() } + + val imageAnalysis = remember { + ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + } + + val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() } + + LaunchedEffect(Unit) { + val cameraProvider = ProcessCameraProvider.getInstance(context).get() + + val barcodeScanner = BarcodeScanning.getClient() + + imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> + val inputImage = InputImage.fromMediaImage( + imageProxy.image!!, + imageProxy.imageInfo.rotationDegrees + ) + + barcodeScanner.process(inputImage) + .addOnSuccessListener { barcodes -> + for (barcode in barcodes) { + when (barcode.valueType) { + Barcode.TYPE_TEXT -> { + val rawValue = barcode.rawValue + if (rawValue != null && rawValue.startsWith("otpauth://")) { + onCodeScanned(rawValue) + return@addOnSuccessListener + } + } + } + } + } + .addOnCompleteListener { + imageProxy.close() + } + } + + try { + cameraProvider.unbindAll() + camera = cameraProvider.bindToLifecycle( + lifecycleOwner, + selector, + preview, + imageAnalysis + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + + DisposableEffect(Unit) { + onDispose { + cameraExecutor.shutdown() + } + } + + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + factory = { context -> + PreviewView(context).apply { + this.scaleType = PreviewView.ScaleType.FILL_CENTER + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + } + }, + modifier = Modifier.fillMaxSize(), + update = { previewView -> + preview.setSurfaceProvider(previewView.surfaceProvider) + } + ) + + // 扫描框覆盖层 + ScanOverlay() + + // 顶部控制栏 + Row( + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "关闭", + tint = Color.White + ) + } + + IconButton( + onClick = { + camera?.let { + if (it.cameraInfo.hasFlashUnit()) { + it.cameraControl.enableTorch(!flashEnabled) + flashEnabled = !flashEnabled + } + } + } + ) { + Icon( + imageVector = if (flashEnabled) Icons.Default.FlashOn else Icons.Default.FlashOff, + contentDescription = if (flashEnabled) "关闭闪光灯" else "打开闪光灯", + tint = Color.White + ) + } + } + + // 底部说明文字 + Card( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color.Black.copy(alpha = 0.7f) + ) + ) { + Text( + text = "将二维码对准扫描框", + modifier = Modifier.padding(16.dp), + color = Color.White, + textAlign = TextAlign.Center, + fontSize = 16.sp + ) + } + } +} + +@Composable +fun ScanOverlay() { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + ) { + // 扫描框 + Box( + modifier = Modifier + .size(280.dp) + .align(Alignment.Center) + .border( + width = 3.dp, + color = Color.White, + shape = RoundedCornerShape(16.dp) + ) + .clip(RoundedCornerShape(16.dp)) + ) + + // 四个角的装饰 + val cornerSize = 24.dp + val cornerThickness = 4.dp + val boxSize = 280.dp + val offsetFromCenter = (boxSize - cornerSize) / 2 + + // 左上角 + Box( + modifier = Modifier + .align(Alignment.Center) + .offset(x = -offsetFromCenter, y = -offsetFromCenter) + .size(cornerSize) + .border( + width = cornerThickness, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(topStart = 8.dp) + ) + ) + + // 右上角 + Box( + modifier = Modifier + .align(Alignment.Center) + .offset(x = offsetFromCenter, y = -offsetFromCenter) + .size(cornerSize) + .border( + width = cornerThickness, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(topEnd = 8.dp) + ) + ) + + // 左下角 + Box( + modifier = Modifier + .align(Alignment.Center) + .offset(x = -offsetFromCenter, y = offsetFromCenter) + .size(cornerSize) + .border( + width = cornerThickness, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(bottomStart = 8.dp) + ) + ) + + // 右下角 + Box( + modifier = Modifier + .align(Alignment.Center) + .offset(x = offsetFromCenter, y = offsetFromCenter) + .size(cornerSize) + .border( + width = cornerThickness, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(bottomEnd = 8.dp) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/sync/SyncManager.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/sync/SyncManager.kt new file mode 100644 index 0000000..62b1cc8 --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/sync/SyncManager.kt @@ -0,0 +1,129 @@ +package cn.deepfal.band.TOTPauthenticator.sync + +import android.content.Context +import android.widget.Toast +import cn.deepfal.band.TOTPauthenticator.data.TOTPInfo +import cn.deepfal.band.TOTPauthenticator.utils.LocalAccountManager +import com.xiaomi.xms.wearable.message.MessageApi +import com.xiaomi.xms.wearable.message.OnMessageReceivedListener +import com.xiaomi.xms.wearable.node.Node +import com.xiaomi.xms.wearable.node.NodeApi +import org.json.JSONObject + +data class SyncAccount( + val account: TOTPInfo, + val status: SyncStatus +) + +enum class SyncStatus { + PHONE_ONLY, // 仅在手机 + WATCH_ONLY, // 仅在手环 + SYNCED // 已同步 +} + +class SyncManager( + private val context: Context, + private val nodeApi: NodeApi, + private val messageApi: MessageApi +) { + private val localAccountManager = LocalAccountManager(context) + private var nodeId: String? = null + private var currentNode: Node? = null + + // 消息监听器 + private val messageListener = OnMessageReceivedListener { deviceId, message -> + try { + val jsonString = String(message) + val jsonObject = JSONObject(jsonString) + // 处理来自手环的消息 + } catch (e: Exception) { + e.printStackTrace() + } + } + + suspend fun initializeConnection(): Boolean { + return try { + val task = nodeApi.connectedNodes + task.addOnSuccessListener { nodes -> + if (nodes.isNotEmpty()) { + currentNode = nodes[0] + nodeId = currentNode?.id + setupMessageListener() + } + } + true + } catch (e: Exception) { + false + } + } + + private fun setupMessageListener() { + nodeId?.let { id -> + messageApi.addListener(id, messageListener) + } + } + + fun getPhoneAccounts(): List { + return localAccountManager.loadAccounts() + } + + suspend fun getWatchAccounts(): List { + // 发送请求到手环获取账户列表 + // 这里需要等待异步响应 + return emptyList() // 临时返回空列表 + } + + fun getMergedAccounts(phoneAccounts: List, watchAccounts: List): List { + val merged = mutableListOf() + val phoneMap = phoneAccounts.associateBy { "${it.name}_${it.key}" } + val watchMap = watchAccounts.associateBy { "${it.name}_${it.key}" } + + // 添加手机账户 + phoneAccounts.forEach { account -> + val key = "${account.name}_${account.key}" + val status = if (watchMap.containsKey(key)) { + SyncStatus.SYNCED + } else { + SyncStatus.PHONE_ONLY + } + merged.add(SyncAccount(account, status)) + } + + // 添加仅手环账户 + watchAccounts.forEach { account -> + val key = "${account.name}_${account.key}" + if (!phoneMap.containsKey(key)) { + merged.add(SyncAccount(account, SyncStatus.WATCH_ONLY)) + } + } + + return merged.sortedBy { it.account.name } + } + + suspend fun syncToWatch(accounts: List): Boolean { + return try { + nodeId?.let { id -> + val jsonMessage = """{"list":${accounts.joinToString(",", "[", "]") { it.toJsonString() }}}""" + val task = messageApi.sendMessage(id, jsonMessage.toByteArray()) + task.addOnSuccessListener { + // 同步成功 + } + true + } ?: false + } catch (e: Exception) { + false + } + } + + fun syncToPhone(accounts: List) { + accounts.forEach { account -> + localAccountManager.addAccount(account) + } + } + + fun cleanup() { + nodeId?.let { id -> + messageApi.removeListener(id) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Color.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Color.kt new file mode 100644 index 0000000..254bd4f --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package cn.deepfal.band.TOTPauthenticator.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Theme.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Theme.kt new file mode 100644 index 0000000..3f48162 --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Theme.kt @@ -0,0 +1,59 @@ +package cn.deepfal.band.TOTPauthenticator.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 +) + +@Composable +fun BandTOTPAPPTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Type.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Type.kt new file mode 100644 index 0000000..2eabac2 --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/ui/theme/Type.kt @@ -0,0 +1,17 @@ +package cn.deepfal.band.TOTPauthenticator.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/LocalAccountManager.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/LocalAccountManager.kt new file mode 100644 index 0000000..ea5b63d --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/LocalAccountManager.kt @@ -0,0 +1,160 @@ +package cn.deepfal.band.TOTPauthenticator.utils + +import android.content.Context +import android.content.SharedPreferences +import cn.deepfal.band.TOTPauthenticator.data.TOTPInfo +import org.json.JSONArray +import org.json.JSONObject + +/** + * 本地账户存储管理器 + * 使用SharedPreferences存储TOTP账户信息 + */ +class LocalAccountManager(private val context: Context) { + + companion object { + private const val PREFS_NAME = "totp_accounts" + private const val KEY_ACCOUNTS = "accounts" + } + + private val sharedPrefs: SharedPreferences = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + /** + * 保存账户列表 + */ + fun saveAccounts(accounts: List) { + try { + val jsonArray = JSONArray() + accounts.forEach { account -> + val jsonObject = JSONObject().apply { + put("name", account.name) + put("usr", account.usr) + put("key", account.key) + put("algorithm", account.algorithm) + put("digits", account.digits) + put("period", account.period) + put("type", account.type) + } + jsonArray.put(jsonObject) + } + + sharedPrefs.edit() + .putString(KEY_ACCOUNTS, jsonArray.toString()) + .apply() + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * 加载账户列表 + */ + fun loadAccounts(): List { + return try { + val jsonString = sharedPrefs.getString(KEY_ACCOUNTS, null) ?: return emptyList() + val jsonArray = JSONArray(jsonString) + val accounts = mutableListOf() + + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.getJSONObject(i) + val account = TOTPInfo( + name = jsonObject.getString("name"), + usr = jsonObject.getString("usr"), + key = jsonObject.getString("key"), + algorithm = jsonObject.optString("algorithm", "SHA1"), + digits = jsonObject.optInt("digits", 6), + period = jsonObject.optInt("period", 30), + type = jsonObject.optString("type", "totp") + ) + accounts.add(account) + } + + accounts + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } + } + + /** + * 添加账户 + */ + fun addAccount(account: TOTPInfo) { + val currentAccounts = loadAccounts().toMutableList() + + // 检查是否已存在相同的账户(基于secret和name) + val existingIndex = currentAccounts.indexOfFirst { + it.key == account.key && it.name == account.name + } + + if (existingIndex >= 0) { + // 更新现有账户 + currentAccounts[existingIndex] = account + } else { + // 添加新账户 + currentAccounts.add(account) + } + + saveAccounts(currentAccounts) + } + + /** + * 删除账户 + */ + fun removeAccount(account: TOTPInfo) { + val currentAccounts = loadAccounts().toMutableList() + currentAccounts.removeAll { + it.key == account.key && it.name == account.name + } + saveAccounts(currentAccounts) + } + + /** + * 更新账户信息(支持修改服务名和用户名) + */ + fun updateAccount(originalAccount: TOTPInfo, newServiceName: String, newUsername: String) { + val currentAccounts = loadAccounts().toMutableList() + val targetIndex = currentAccounts.indexOfFirst { + it.key == originalAccount.key && it.name == originalAccount.name && it.usr == originalAccount.usr + } + + if (targetIndex >= 0) { + // 创建更新后的账户信息 + val updatedAccount = originalAccount.copy( + name = newServiceName, + usr = newUsername + ) + currentAccounts[targetIndex] = updatedAccount + saveAccounts(currentAccounts) + } + } + + /** + * 清空所有账户 + */ + fun clearAllAccounts() { + sharedPrefs.edit().remove(KEY_ACCOUNTS).apply() + } + + /** + * 批量添加账户 + */ + fun addAccounts(newAccounts: List) { + val currentAccounts = loadAccounts().toMutableList() + + newAccounts.forEach { newAccount -> + val existingIndex = currentAccounts.indexOfFirst { + it.key == newAccount.key && it.name == newAccount.name + } + + if (existingIndex >= 0) { + currentAccounts[existingIndex] = newAccount + } else { + currentAccounts.add(newAccount) + } + } + + saveAccounts(currentAccounts) + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/TOTPGenerator.kt b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/TOTPGenerator.kt new file mode 100644 index 0000000..816f8ce --- /dev/null +++ b/app/src/main/java/cn/deepfal/band/TOTPauthenticator/utils/TOTPGenerator.kt @@ -0,0 +1,198 @@ +package cn.deepfal.band.TOTPauthenticator.utils + +import java.nio.ByteBuffer +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.math.pow + +/** + * TOTP (Time-based One-Time Password) 生成器 + * 基于RFC 6238标准实现 + */ +object TOTPGenerator { + + /** + * 生成TOTP验证码 + * @param secret Base32编码的密钥 + * @param algorithm 算法类型 (SHA1, SHA256, SHA512) + * @param digits 验证码位数 (通常是6位) + * @param period 时间周期 (通常是30秒) + * @return 生成的验证码 + */ + fun generateTOTP( + secret: String, + algorithm: String = "SHA1", + digits: Int = 6, + period: Int = 30 + ): String { + return try { + val key = decodeBase32(secret) + val timeCounter = System.currentTimeMillis() / 1000 / period + val hmac = generateHMAC(key, timeCounter, algorithm) + val code = truncate(hmac, digits) + code.toString().padStart(digits, '0') + } catch (e: Exception) { + "000000" // 出错时返回默认值 + } + } + + /** + * 获取当前时间步长内剩余的秒数 + * @param period 时间周期 + * @return 剩余秒数 + */ + fun getRemainingSeconds(period: Int = 30): Int { + val currentTime = System.currentTimeMillis() / 1000 + return period - (currentTime % period).toInt() + } + + /** + * 获取当前时间步长的进度百分比 + * @param period 时间周期 + * @return 进度百分比 (0.0 - 1.0) + */ + fun getProgress(period: Int = 30): Float { + val currentTime = System.currentTimeMillis() / 1000 + val elapsed = (currentTime % period).toInt() + return elapsed.toFloat() / period.toFloat() + } + + /** + * Base32解码 + */ + private fun decodeBase32(base32: String): ByteArray { + val cleanInput = base32.replace(" ", "").replace("=", "").uppercase() + val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + val output = mutableListOf() + + var buffer = 0 + var bitsLeft = 0 + + for (char in cleanInput) { + val index = alphabet.indexOf(char) + if (index == -1) continue + + buffer = (buffer shl 5) or index + bitsLeft += 5 + + if (bitsLeft >= 8) { + output.add((buffer shr (bitsLeft - 8)).toByte()) + bitsLeft -= 8 + } + } + + return output.toByteArray() + } + + /** + * 生成HMAC + */ + private fun generateHMAC(key: ByteArray, counter: Long, algorithm: String): ByteArray { + val algorithmName = when (algorithm.uppercase()) { + "SHA1" -> "HmacSHA1" + "SHA256" -> "HmacSHA256" + "SHA512" -> "HmacSHA512" + else -> "HmacSHA1" + } + + val mac = Mac.getInstance(algorithmName) + val keySpec = SecretKeySpec(key, algorithmName) + mac.init(keySpec) + + val counterBytes = ByteBuffer.allocate(8).putLong(counter).array() + return mac.doFinal(counterBytes) + } + + /** + * 动态截取 + */ + private fun truncate(hmac: ByteArray, digits: Int): Int { + val offset = (hmac[hmac.size - 1].toInt() and 0x0F) + + val code = ((hmac[offset].toInt() and 0x7F) shl 24) or + ((hmac[offset + 1].toInt() and 0xFF) shl 16) or + ((hmac[offset + 2].toInt() and 0xFF) shl 8) or + (hmac[offset + 3].toInt() and 0xFF) + + return code % (10.0.pow(digits).toInt()) + } +} + +/** + * Steam TOTP 生成器 + * Steam使用特殊的字符集和算法,密钥为Base32编码(从URI中提取) + */ +object SteamTOTPGenerator { + private const val STEAM_CHARS = "23456789BCDFGHJKMNPQRTVWXY" + + fun generateSteamCode(secret: String): String { + return try { + // 🔄 修复:现在Steam使用URI中的Base32编码的secret + val key = decodeBase32(secret) + + val timeDiv30 = System.currentTimeMillis() / 1000 / 30 + + // 创建8字节的时间计数器 + val timeBytes = ByteArray(8) + timeBytes[4] = ((timeDiv30 shr 24) and 0xFF).toByte() + timeBytes[5] = ((timeDiv30 shr 16) and 0xFF).toByte() + timeBytes[6] = ((timeDiv30 shr 8) and 0xFF).toByte() + timeBytes[7] = (timeDiv30 and 0xFF).toByte() + + // 生成HMAC-SHA1 + val mac = Mac.getInstance("HmacSHA1") + val keySpec = SecretKeySpec(key, "HmacSHA1") + mac.init(keySpec) + val hmacBytes = mac.doFinal(timeBytes) + + // 获取偏移量 (最后一个字节的低4位) + val offset = (hmacBytes[19].toInt() and 0x0F) + + // 截取4字节并转换为32位整数 + var fullcode = ((hmacBytes[offset].toInt() and 0xFF) shl 24) or + ((hmacBytes[offset + 1].toInt() and 0xFF) shl 16) or + ((hmacBytes[offset + 2].toInt() and 0xFF) shl 8) or + (hmacBytes[offset + 3].toInt() and 0xFF) + + // 取31位 (去掉符号位) + fullcode = fullcode and 0x7FFFFFFF + + // 生成5位Steam验证码 + val result = StringBuilder() + for (i in 0 until 5) { + result.append(STEAM_CHARS[fullcode % STEAM_CHARS.length]) + fullcode /= STEAM_CHARS.length + } + + result.toString() + } catch (e: Exception) { + "23456" // Steam默认5位字符 + } + } + + private fun decodeBase32(base32: String): ByteArray { + val cleanInput = base32.replace(" ", "").replace("=", "").uppercase() + val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + val output = mutableListOf() + + var buffer = 0 + var bitsLeft = 0 + + for (char in cleanInput) { + val index = alphabet.indexOf(char) + if (index == -1) continue + + buffer = (buffer shl 5) or index + bitsLeft += 5 + + if (bitsLeft >= 8) { + output.add((buffer shr (bitsLeft - 8)).toByte()) + bitsLeft -= 8 + } + } + + return output.toByteArray() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..af4a905d0315c49d34cff3477f072a4d65ee6f83 GIT binary patch literal 4561 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 P8nS0PhT3&thuHrG(bU`C literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c48b06beade3dcf2ba477303d2f55eb556d54f00 GIT binary patch literal 3026 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 uylDgu{x#?N@z)~azrpS;6MM+s-05TDGs6(cSg{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..e466b6ad8b9da30a8c43beb26c9802e27b3d8a4d GIT binary patch literal 5307 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..dbbb63d2e41f5fe75ac239d5079c878e120abe4d GIT binary patch literal 7231 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..de593ea1eebe6227f00cd8a09ac24505f1f8e938 GIT binary patch literal 10157 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..b5cd46e --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.png b/app/src/main/res/drawable/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c48b06beade3dcf2ba477303d2f55eb556d54f00 GIT binary patch literal 3026 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 uylDgu{x#?N@z)~azrpS;6MM+s-05TDGs6(cSg{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..16313e0 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..e416e1c --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_first.xml b/app/src/main/res/layout/fragment_first.xml new file mode 100644 index 0000000..44baecd --- /dev/null +++ b/app/src/main/res/layout/fragment_first.xml @@ -0,0 +1,35 @@ + + + + + +