From e606ed6f0bad1d59874629ba239f0e90f1266cee Mon Sep 17 00:00:00 2001 From: zzh Date: Wed, 26 Nov 2025 10:30:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4=EF=BC=9A?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E5=AE=B6=E5=B1=85=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能特性: - 玻璃态/新拟态设计风格的现代UI - 侧边栏导航(控制台、最近、收藏、下载、支持、设置) - 房间管理:可添加、删除房间,支持横向滚动 - 长按房间标签进入编辑模式,带旋转晃动动画 - 三个场景模式按钮(回家、出门、玩乐) - 空调控制卡片 - 使用状态图表 - 灯光控制 - 设备网格展示 - 设置页面(账户、设备管理、显示设置、关于) - 使用SharedPreferences持久化存储房间列表 - 响应式设计,支持多种屏幕尺寸 --- .gitignore | 81 ++ app/build.gradle.kts | 80 ++ app/proguard-rules.pro | 1 + .../smarthome/SmartHomeStabilityTest.kt | 24 + .../com/example/smarthome/UiAdaptationTest.kt | 25 + app/src/main/AndroidManifest.xml | 15 + .../com/example/smarthome/MainActivity.kt | 102 ++ .../com/example/smarthome/SmartHomeApp.kt | 5 + .../example/smarthome/ui/LiquidIndicator.kt | 32 + .../com/example/smarthome/ui/MainScaffold.kt | 908 ++++++++++++++++++ .../example/smarthome/ui/NeumorphButton.kt | 57 ++ .../example/smarthome/ui/NeumorphControls.kt | 46 + .../java/com/example/smarthome/ui/Previews.kt | 25 + .../example/smarthome/ui/SettingsScreen.kt | 357 +++++++ .../com/example/smarthome/ui/theme/Theme.kt | 24 + .../main/res/drawable/bg_radial_overlay.xml | 14 + app/src/main/res/drawable/ic_ac.xml | 4 + app/src/main/res/drawable/ic_bookmark.xml | 9 + app/src/main/res/drawable/ic_curtain.xml | 5 + app/src/main/res/drawable/ic_dashboard.xml | 9 + app/src/main/res/drawable/ic_download.xml | 9 + .../res/drawable/ic_launcher_background.xml | 3 + .../res/drawable/ic_launcher_foreground.xml | 4 + app/src/main/res/drawable/ic_light.xml | 4 + app/src/main/res/drawable/ic_media.xml | 4 + app/src/main/res/drawable/ic_recent.xml | 9 + app/src/main/res/drawable/ic_room1.xml | 3 + app/src/main/res/drawable/ic_room2.xml | 3 + app/src/main/res/drawable/ic_room3.xml | 3 + app/src/main/res/drawable/ic_room4.xml | 3 + app/src/main/res/drawable/ic_room5.xml | 3 + app/src/main/res/drawable/ic_security.xml | 3 + app/src/main/res/drawable/ic_settings.xml | 9 + app/src/main/res/drawable/ic_support.xml | 9 + app/src/main/res/layout/activity_main.xml | 27 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 + app/src/main/res/raw/design_tokens.json | 34 + app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/themes.xml | 9 + build.gradle.kts | 8 + gradle.properties | 4 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43739 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 +++++ gradlew.bat | 94 ++ settings.gradle.kts | 20 + 46 files changed, 2358 insertions(+) create mode 100644 .gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/smarthome/SmartHomeStabilityTest.kt create mode 100644 app/src/androidTest/java/com/example/smarthome/UiAdaptationTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/smarthome/MainActivity.kt create mode 100644 app/src/main/java/com/example/smarthome/SmartHomeApp.kt create mode 100644 app/src/main/java/com/example/smarthome/ui/LiquidIndicator.kt create mode 100644 app/src/main/java/com/example/smarthome/ui/MainScaffold.kt create mode 100644 app/src/main/java/com/example/smarthome/ui/NeumorphButton.kt create mode 100644 app/src/main/java/com/example/smarthome/ui/NeumorphControls.kt create mode 100644 app/src/main/java/com/example/smarthome/ui/Previews.kt create mode 100644 app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt create mode 100644 app/src/main/java/com/example/smarthome/ui/theme/Theme.kt create mode 100644 app/src/main/res/drawable/bg_radial_overlay.xml create mode 100644 app/src/main/res/drawable/ic_ac.xml create mode 100644 app/src/main/res/drawable/ic_bookmark.xml create mode 100644 app/src/main/res/drawable/ic_curtain.xml create mode 100644 app/src/main/res/drawable/ic_dashboard.xml create mode 100644 app/src/main/res/drawable/ic_download.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_light.xml create mode 100644 app/src/main/res/drawable/ic_media.xml create mode 100644 app/src/main/res/drawable/ic_recent.xml create mode 100644 app/src/main/res/drawable/ic_room1.xml create mode 100644 app/src/main/res/drawable/ic_room2.xml create mode 100644 app/src/main/res/drawable/ic_room3.xml create mode 100644 app/src/main/res/drawable/ic_room4.xml create mode 100644 app/src/main/res/drawable/ic_room5.xml create mode 100644 app/src/main/res/drawable/ic_security.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_support.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/raw/design_tokens.json create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..295943b --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.zip +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ + +# Android Profiling +*.hprof + +# macOS +.DS_Store diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..52aecde --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,80 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.example.smarthome" + compileSdk = 35 + + defaultConfig { + applicationId = "com.example.smarthome" + minSdk = 24 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + vectorDrawables { + useSupportLibrary = true + } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + debug { + isDebuggable = true + } + } + + buildFeatures { + compose = true + viewBinding = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.14" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("com.google.android.material:material:1.12.0") + + implementation(platform("androidx.compose:compose-bom:2024.10.00")) + implementation("androidx.activity:activity-compose:1.9.3") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material3:material3-window-size-class:1.3.0") + + implementation("com.github.Dimezis:BlurView:version-3.2.0") + + debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") + + androidTestImplementation(platform("androidx.compose:compose-bom:2024.10.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-test-manifest") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..432c1e1 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ +# Keep LeakCanary in debug only; no rules needed for release here diff --git a/app/src/androidTest/java/com/example/smarthome/SmartHomeStabilityTest.kt b/app/src/androidTest/java/com/example/smarthome/SmartHomeStabilityTest.kt new file mode 100644 index 0000000..17dd684 --- /dev/null +++ b/app/src/androidTest/java/com/example/smarthome/SmartHomeStabilityTest.kt @@ -0,0 +1,24 @@ +package com.example.smarthome + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class SmartHomeStabilityTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun continuousClickTest() { + repeat(1000) { + composeRule.onNodeWithText("灯光").performClick() + } + } +} diff --git a/app/src/androidTest/java/com/example/smarthome/UiAdaptationTest.kt b/app/src/androidTest/java/com/example/smarthome/UiAdaptationTest.kt new file mode 100644 index 0000000..fc0089a --- /dev/null +++ b/app/src/androidTest/java/com/example/smarthome/UiAdaptationTest.kt @@ -0,0 +1,25 @@ +package com.example.smarthome + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.unit.Density +import com.example.smarthome.ui.ControlPanel +import org.junit.Rule +import org.junit.Test + +class UiAdaptationTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun dpiAdaptation160to640() { + listOf(160f, 240f, 320f, 480f, 640f).forEach { dpi -> + rule.setContent { + CompositionLocalProvider(LocalDensity provides Density(dpi / 160f)) { + ControlPanel(roomIndex = 0) + } + } + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3a337ce --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/src/main/java/com/example/smarthome/MainActivity.kt b/app/src/main/java/com/example/smarthome/MainActivity.kt new file mode 100644 index 0000000..5988570 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/MainActivity.kt @@ -0,0 +1,102 @@ +package com.example.smarthome + +import android.graphics.drawable.Drawable +import android.os.Bundle +import androidx.activity.ComponentActivity +import com.example.smarthome.ui.theme.SmartHomeTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ViewCompositionStrategy +import eightbitlab.com.blurview.BlurTarget +import eightbitlab.com.blurview.BlurView +import androidx.compose.material3.Surface +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.foundation.layout.Box +import androidx.compose.ui.unit.dp +import com.example.smarthome.ui.MainScaffold +import androidx.compose.ui.platform.ComposeView + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, true) + + setContentView(R.layout.activity_main) + val composeView = findViewById(R.id.composeView) + composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + composeView.setContent { + SmartHomeTheme { + Surface(modifier = Modifier.fillMaxSize(), color = Color(0xFF121212)) { + AppRoot() + } + } + } + + val decorView = window.decorView + val blurTarget = findViewById(R.id.blurTarget) + val blurView = findViewById(R.id.blurView) + val windowBackground: Drawable? = decorView.background + blurView.setupWith(blurTarget) + .setFrameClearDrawable(windowBackground) + .setBlurRadius(20f) + + } +} + +@Composable +fun AppRoot() { + val context = androidx.compose.ui.platform.LocalContext.current + val sharedPreferences = remember { + context.getSharedPreferences("smart_home_prefs", android.content.Context.MODE_PRIVATE) + } + + // 从SharedPreferences加载房间列表 + val defaultRooms = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房") + val savedRooms = remember { + val saved = sharedPreferences.getString("rooms", null) + if (saved != null) { + saved.split(",").filter { it.isNotBlank() } + } else { + defaultRooms + } + } + + var selectedRoom by remember { mutableStateOf(0) } + var selectedNavItem by remember { mutableStateOf(0) } // 0=控制台, 5=设置 + var rooms by remember { mutableStateOf(savedRooms) } + + // 保存房间列表到SharedPreferences + fun saveRooms(roomList: List) { + sharedPreferences.edit().putString("rooms", roomList.joinToString(",")).apply() + rooms = roomList + } + + Box(modifier = Modifier.fillMaxSize()) { + MainScaffold( + selectedRoom = selectedRoom, + onRoomSelect = { selectedRoom = it }, + selectedNavItem = selectedNavItem, + onNavItemSelect = { selectedNavItem = it }, + rooms = rooms, + onAddRoom = { newRoomName -> + saveRooms(rooms + newRoomName) + }, + onDeleteRoom = { index -> + if (rooms.size > 1) { // 至少保留一个房间 + saveRooms(rooms.filterIndexed { i, _ -> i != index }) + if (selectedRoom >= rooms.size - 1) { + selectedRoom = (rooms.size - 2).coerceAtLeast(0) + } + } + } + ) + } +} + + diff --git a/app/src/main/java/com/example/smarthome/SmartHomeApp.kt b/app/src/main/java/com/example/smarthome/SmartHomeApp.kt new file mode 100644 index 0000000..a3d9a22 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/SmartHomeApp.kt @@ -0,0 +1,5 @@ +package com.example.smarthome + +import android.app.Application + +class SmartHomeApp : Application() diff --git a/app/src/main/java/com/example/smarthome/ui/LiquidIndicator.kt b/app/src/main/java/com/example/smarthome/ui/LiquidIndicator.kt new file mode 100644 index 0000000..344af08 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/LiquidIndicator.kt @@ -0,0 +1,32 @@ +package com.example.smarthome.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.unit.dp +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size + +@Composable +fun LiquidIndicator(visible: Boolean) { + val t by animateFloatAsState(targetValue = if (visible) 1f else 0f, animationSpec = spring(stiffness = Spring.StiffnessMediumLow), label = "t") + if (t <= 0f) return + Canvas(modifier = Modifier) { + val w = size.width + val h = size.height + val p = Path() + val r = h.coerceAtMost(w) / 2f + p.addOval(androidx.compose.ui.geometry.Rect(Offset(w / 2f - r / 2f, h / 2f - r / 2f), Size(r, r))) + drawIntoCanvas { + drawPath(p, Color(0x55FFFFFF)) + } + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt new file mode 100644 index 0000000..ef209e4 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt @@ -0,0 +1,908 @@ +package com.example.smarthome.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.example.smarthome.R +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.foundation.layout.fillMaxHeight +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import android.view.SoundEffectConstants +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.geometry.Offset +import androidx.compose.foundation.layout.offset +import androidx.compose.ui.draw.rotate +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode + +@Composable +fun MainScaffold( + selectedRoom: Int, + onRoomSelect: (Int) -> Unit, + selectedNavItem: Int = 0, + onNavItemSelect: (Int) -> Unit = {}, + rooms: List = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"), + onAddRoom: (String) -> Unit = {}, + onDeleteRoom: (Int) -> Unit = {} +) { + Row(modifier = Modifier.fillMaxSize().background(Color(0xFF121212))) { + SideNavRail( + selectedNavItem = selectedNavItem, + onNavItemSelect = onNavItemSelect + ) + + // 根据选中的导航项显示不同的内容 + when (selectedNavItem) { + 0 -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom) + 5 -> SettingsContent() + else -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom) + } + } +} + +@Composable +fun DashboardContent( + selectedRoom: Int, + onRoomSelect: (Int) -> Unit, + rooms: List = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"), + onAddRoom: (String) -> Unit = {}, + onDeleteRoom: (Int) -> Unit = {} +) { + var showAddRoomDialog by remember { mutableStateOf(false) } + var editMode by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(editMode) { + if (editMode) { + detectTapGestures( + onTap = { editMode = false } + ) + } + } + ) { + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + TopBar() + RoomTabs( + selectedRoom = selectedRoom, + onRoomSelect = onRoomSelect, + rooms = rooms, + onAddRoomClick = { showAddRoomDialog = true }, + onDeleteRoom = onDeleteRoom, + editMode = editMode, + onEditModeChange = { editMode = it } + ) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { + AirConditionerCard(modifier = Modifier.weight(1f)) + UsageStatusChart(modifier = Modifier.weight(1f)) + } + LightRow(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) + ModeButtonsRow(onModeSelected = { }) + MyDevicesGrid(selectedRoom = selectedRoom, modifier = Modifier.fillMaxSize()) + } + + if (showAddRoomDialog) { + AddRoomDialog( + onDismiss = { showAddRoomDialog = false }, + onConfirm = { roomName -> + onAddRoom(roomName) + showAddRoomDialog = false + } + ) + } + } +} + +@Composable +fun HeaderHero() { + TopBar() +} + +@Composable +fun StatusChips() { + SideNavRail() +} + +@Composable +fun Chip(text: String) { + Box(modifier = Modifier.clip(RoundedCornerShape(20.dp)).background(Color(0x331A1A1A)).padding(horizontal = 12.dp, vertical = 8.dp)) { + Text(text = text, color = Color.White) + } +} + +@Composable +fun QuickActions() { + Row(modifier = Modifier.fillMaxWidth().padding(24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + QuickActionCard( + title = "回家模式", + colors = Brush.linearGradient(listOf(Color(0xFFFFD54F), Color(0xFFFF8F00))), + modifier = Modifier.weight(1f), + onClick = { /* TODO: 触发回家场景 */ } + ) + QuickActionCard( + title = "出门模式", + colors = Brush.linearGradient(listOf(Color(0xFFFF8A65), Color(0xFFFF7043))), + modifier = Modifier.weight(1f), + onClick = { /* TODO: 触发出门场景 */ } + ) + QuickActionCard( + title = "玩乐模式", + colors = Brush.linearGradient(listOf(Color(0xFF6A3DFF), Color(0xFF3A0CA3))), + modifier = Modifier.weight(1f), + onClick = { /* TODO: 触发玩乐场景 */ } + ) + } +} + +@Composable +fun QuickActionCard(title: String, colors: Brush, modifier: Modifier = Modifier, onClick: () -> Unit) { + val shape = RoundedCornerShape(24.dp) + var pressed by remember { mutableStateOf(false) } + val scale by androidx.compose.animation.core.animateFloatAsState( + targetValue = if (pressed) 0.97f else 1f, + animationSpec = androidx.compose.animation.core.spring(), + label = "quick_action_scale" + ) + Box( + modifier = modifier + .height(88.dp) + .shadow(12.dp, shape, false) + .clip(shape) + .background(colors) + .scale(scale) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + pressed = !pressed + onClick() + } + .padding(16.dp), + contentAlignment = Alignment.BottomStart + ) { + Text(text = title, fontWeight = FontWeight.Medium, color = Color.White) + } +} + +@Composable +fun RoomTabs( + selectedRoom: Int, + onRoomSelect: (Int) -> Unit, + rooms: List = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"), + onAddRoomClick: () -> Unit = {}, + onDeleteRoom: (Int) -> Unit = {}, + editMode: Boolean = false, + onEditModeChange: (Boolean) -> Unit = {} +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 可滚动的房间标签区域 + Row( + modifier = Modifier + .weight(1f) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + rooms.forEachIndexed { index, name -> + RoomTab( + name = name, + selected = index == selectedRoom, + editMode = editMode, + canDelete = rooms.size > 1, + onClick = { + if (!editMode) { + onRoomSelect(index) + } + }, + onLongClick = { onEditModeChange(true) }, + onDelete = { + onDeleteRoom(index) + onEditModeChange(false) + } + ) + } + } + + // 固定的添加按钮 + GradientButton("+ 添加", onClick = onAddRoomClick) + } +} + +@Composable +fun RoomTab( + name: String, + selected: Boolean, + editMode: Boolean, + canDelete: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, + onDelete: () -> Unit +) { + val scope = rememberCoroutineScope() + + // 改进的晃动动画 - 使用无限循环的旋转动画 + val infiniteTransition = rememberInfiniteTransition(label = "shake") + val rotation by infiniteTransition.animateFloat( + initialValue = -3f, + targetValue = 3f, + animationSpec = infiniteRepeatable( + animation = tween(150, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "rotation" + ) + + // 缩放动画 + val scale by androidx.compose.animation.core.animateFloatAsState( + targetValue = if (editMode && canDelete) 0.95f else 1f, + animationSpec = androidx.compose.animation.core.spring( + dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy, + stiffness = androidx.compose.animation.core.Spring.StiffnessLow + ), + label = "scale" + ) + + Box( + modifier = Modifier + .scale(scale) + .then( + if (editMode && canDelete) { + Modifier.rotate(rotation) + } else { + Modifier + } + ) + .pointerInput(Unit) { + detectTapGestures( + onTap = { onClick() }, + onLongPress = { + if (canDelete) { + onLongClick() + } + } + ) + } + ) { + val base = Modifier.clip(RoundedCornerShape(20.dp)) + val mod = if (selected) { + base.background(brush = Brush.linearGradient(listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)))) + } else { + base.background(color = Color(0x22121212)) + } + + Box( + modifier = mod.padding(horizontal = 14.dp, vertical = 10.dp) + ) { + Text( + text = name, + color = if (selected) Color.Black else Color(0xFFB0B0B0), + maxLines = 1 + ) + } + + // 删除按钮 + if (editMode && canDelete) { + Box( + modifier = Modifier + .size(22.dp) + .offset(x = (-4).dp, y = (-4).dp) + .align(Alignment.TopEnd) + .shadow(4.dp, RoundedCornerShape(11.dp)) + .clip(RoundedCornerShape(11.dp)) + .background(Color(0xFFFF5252)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onDelete() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "×", + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } + } + } +} + +@Composable +fun DeviceGrid() { + val devices = listOf( + Device("空调", "室内 25℃", R.drawable.ic_ac, true), + Device("智能开关", "待机中", R.drawable.ic_light, false), + Device("吊灯", "6500K", R.drawable.ic_light, true), + Device("扫地机", "清扫中", R.drawable.ic_media, true), + Device("空气净化", "运行中", R.drawable.ic_security, true), + Device("饮水机", "待机中", R.drawable.ic_media, false) + ) + LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 140.dp), modifier = Modifier.fillMaxSize().padding(24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + items(devices) { d -> DeviceCard(d) } + } +} + +data class Device(val name: String, val sub: String, val icon: Int, val on: Boolean) + +@Composable +fun DeviceCard(d: Device) { + var active by remember { mutableStateOf(d.on) } + Box(modifier = Modifier.height(120.dp).clip(RoundedCornerShape(24.dp)).background(Color(0x26121212)).shadow(12.dp, RoundedCornerShape(24.dp), true).padding(16.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Image(painter = painterResource(id = d.icon), contentDescription = null, modifier = Modifier.size(40.dp)) + Column { + Text(text = d.name, fontWeight = FontWeight.SemiBold) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Box(modifier = Modifier.size(8.dp).clip(RoundedCornerShape(4.dp)).background(if (active) Color(0xFF00E676) else Color(0xFF9AA0A6))) + Text(text = d.sub, color = Color(0xFF9AA0A6)) + } + } + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text(text = if (active) "运行中" else "待机中", color = if (active) Color(0xFF00E676) else Color(0xFF9AA0A6)) + Toggle(active) { active = it } + } + } + } +} + +@Composable +fun Toggle(on: Boolean, change: (Boolean) -> Unit) { + Box(modifier = Modifier.size(48.dp, 28.dp).clip(RoundedCornerShape(18.dp)).background(if (on) Color(0xFF3A0CA3) else Color(0x33222222)).clickable { change(!on) }.padding(4.dp)) { + Box(modifier = Modifier.size(20.dp).clip(RoundedCornerShape(10.dp)).background(Color.White).align(if (on) Alignment.CenterEnd else Alignment.CenterStart)) + } +} + +@Composable +fun BottomNav() { + Row(modifier = Modifier.fillMaxWidth().padding(16.dp).clip(RoundedCornerShape(28.dp)).background(Color(0x33121212)).padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly) { + NavItem("首页", true) + NavItem("设备", false) + NavItem("我的", false) + } +} + +@Composable +fun NavItem(title: String, selected: Boolean) { + Box(modifier = Modifier.clip(RoundedCornerShape(20.dp)).background(if (selected) Color(0x55121212) else Color.Transparent).padding(horizontal = 18.dp, vertical = 10.dp)) { + Text(text = title, color = if (selected) Color.White else Color(0xFFB0B0B0)) + } +} + +@Composable +fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) { + Column(modifier = Modifier.width(120.dp).fillMaxHeight().padding(12.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Box(modifier = Modifier.size(48.dp).clip(RoundedCornerShape(16.dp)).background(Color(0x22121212))) + NavRailItem("控制台", R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) }) + NavRailItem("最近", R.drawable.ic_recent, selectedNavItem == 1, onClick = { onNavItemSelect(1) }) + NavRailItem("收藏", R.drawable.ic_bookmark, selectedNavItem == 2, onClick = { onNavItemSelect(2) }) + NavRailItem("下载", R.drawable.ic_download, selectedNavItem == 3, onClick = { onNavItemSelect(3) }) + NavRailItem("支持", R.drawable.ic_support, selectedNavItem == 4, onClick = { onNavItemSelect(4) }) + NavRailItem("设置", R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) }) + } +} + +@Composable +fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Unit = {}) { + val scale by androidx.compose.animation.core.animateFloatAsState( + targetValue = if (selected) 1.05f else 1f, + animationSpec = androidx.compose.animation.core.spring(), + label = "nav_scale" + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier + .scale(scale) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onClick() } + .padding(vertical = 4.dp) + ) { + Box( + modifier = Modifier + .size(48.dp) + .shadow(if (selected) 8.dp else 0.dp, RoundedCornerShape(14.dp)) + .clip(RoundedCornerShape(14.dp)) + .background( + if (selected) { + Brush.linearGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ) + } else { + Brush.linearGradient( + listOf(Color(0x22FFFFFF), Color(0x22FFFFFF)) + ) + } + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = text, + modifier = Modifier.size(24.dp), + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( + if (selected) Color.Black else Color(0xFFB0B0B0) + ) + ) + } + Text( + text = text, + color = if (selected) Color.White else Color(0xFFB0B0B0), + fontSize = 13.sp, + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, + maxLines = 1 + ) + } +} + +@Composable +fun TopBar() { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text(text = "控制台", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.width(280.dp).height(40.dp).clip(RoundedCornerShape(20.dp)).background(Color(0x22121212)).padding(horizontal = 16.dp), contentAlignment = Alignment.CenterStart) { + Text(text = "搜索关键词", color = Color(0xFF9AA0A6)) + } + Box(modifier = Modifier.size(40.dp).clip(RoundedCornerShape(12.dp)).background(Color(0x22121212))) + Box(modifier = Modifier.size(40.dp).clip(RoundedCornerShape(12.dp)).background(Color(0x22121212))) + Box(modifier = Modifier.size(40.dp).clip(RoundedCornerShape(12.dp)).background(Color(0x22121212))) + GradientButton("+ 添加设备") + } + } +} + +@Composable +fun GradientButton(text: String, onClick: () -> Unit = {}) { + Box(modifier = Modifier.height(36.dp).clip(RoundedCornerShape(20.dp)).background(Brush.linearGradient(listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)))).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onClick() }.padding(horizontal = 16.dp), contentAlignment = Alignment.Center) { + Text(text = text, color = Color.Black, fontWeight = FontWeight.Medium) + } +} + +@Composable +fun MyDevicesGrid(selectedRoom: Int, modifier: Modifier = Modifier) { + val devices = when (selectedRoom) { + 1 -> listOf( + Device("智能电视", "已运行 3 小时", R.drawable.ic_media, true), + Device("音响", "已运行 3 小时", R.drawable.ic_media, false), + Device("路由器", "已运行 3 小时", R.drawable.ic_security, true), + Device("无线网络", "已运行 3 小时", R.drawable.ic_security, true), + Device("暖气", "已运行 3 小时", R.drawable.ic_media, false), + Device("插座", "已运行 3 小时", R.drawable.ic_media, true) + ) + else -> emptyList() + } + LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 140.dp), modifier = modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + items(devices) { d -> DeviceCard(d) } + } +} + +@Composable +fun UsageStatusChart(modifier: Modifier = Modifier) { + val values = listOf(12f, 18f, 16f, 30f, 20f, 22f, 17f, 19f, 25f, 32f) + Box(modifier = modifier.height(220.dp).clip(RoundedCornerShape(24.dp)).background(Color(0x26121212)).padding(16.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = "使用状态", fontWeight = FontWeight.SemiBold) + Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) { + Column { Text(text = "35.02kWh"); Text(text = "总消耗", color = Color(0xFF9AA0A6)) } + Column { Text(text = "32h"); Text(text = "总时长", color = Color(0xFF9AA0A6)) } + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Bottom) { + values.forEach { v -> + Box(modifier = Modifier.weight(1f).height(v.dp).clip(RoundedCornerShape(6.dp)).background(Color(0xFF2A2A2A))) + } + } + } + } +} + +@Composable +fun AirConditionerCard(modifier: Modifier = Modifier) { + var temp by remember { mutableStateOf(24f) } + Box(modifier = modifier.height(220.dp).clip(RoundedCornerShape(24.dp)).background(Color(0x26121212)).padding(16.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Text(text = "空调", fontWeight = FontWeight.SemiBold) + Switch(checked = true, onCheckedChange = {}) + } + Text(text = "24°C", style = MaterialTheme.typography.headlineMedium) + Slider(value = temp, onValueChange = { temp = it }, valueRange = 16f..32f) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x22121212))) + Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x22121212))) + Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x22121212))) + Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x22121212))) + } + } + } +} + +@Composable +fun LightList(modifier: Modifier = Modifier) { + val lights = listOf( + "灯光 1" to 0.6f, + "灯光 2" to 0.8f, + "灯光 3" to 0.45f, + "灯光 4" to 0.6f, + "灯光 5" to 0.6f + ) + Column(modifier = modifier.fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(text = "灯光", fontWeight = FontWeight.SemiBold) + lights.forEach { (name, percent) -> + Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clip(RoundedCornerShape(16.dp)).background(Color(0x26121212)).padding(12.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x22121212))) + Column { Text(text = name); Text(text = "${(percent * 100).toInt()}%", color = Color(0xFF9AA0A6)) } + } + DotsProgress(percent) + } + } + } +} + +@Composable +fun DotsProgress(percent: Float) { + val total = 12 + Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { + repeat(total) { i -> + val filled = i < (percent * total).toInt() + Box(modifier = Modifier.size(6.dp).clip(RoundedCornerShape(3.dp)).background(if (filled) Color(0xFFA9F0FF) else Color(0xFF3A3A3A))) + } + } +} + +enum class Mode { HOME, AWAY, FUN } + +@Composable +fun ModeButtonsRow(onModeSelected: (Mode) -> Unit) { + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + val density = LocalDensity.current + val buttonHeight = when { + maxWidth >= 1024.dp -> with(density) { 500f.toDp() } + maxWidth >= 600.dp -> with(density) { 460f.toDp() } + else -> with(density) { 160f.toDp() } + } + + var selected by remember { mutableStateOf(null) } + + val isVertical = maxWidth < 480.dp + val containerModifier = Modifier.fillMaxWidth() + val arrangement = Arrangement.spacedBy(16.dp) + + val items = listOf( + Triple(Mode.HOME, Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFF00C9A7))), "回家"), + Triple(Mode.AWAY, Brush.linearGradient(listOf(Color(0xFFFF8A65), Color(0xFFFF7043))), "出门"), + Triple(Mode.FUN, Brush.linearGradient(listOf(Color(0xFF6A3DFF), Color(0xFF3A0CA3))), "玩乐"), + ) + + if (isVertical) { + Column(modifier = containerModifier, verticalArrangement = arrangement) { + items.forEach { (mode, brush, label) -> + ModeButton( + text = label, + modifier = Modifier.fillMaxWidth(), + height = buttonHeight, + brush = brush, + selected = selected == mode, + onClick = { + selected = mode + onModeSelected(mode) + } + ) + } + } + } else { + Row(modifier = containerModifier, horizontalArrangement = arrangement) { + items.forEach { (mode, brush, label) -> + ModeButton( + text = label, + modifier = Modifier.weight(1f), + height = buttonHeight, + brush = brush, + selected = selected == mode, + onClick = { + selected = mode + onModeSelected(mode) + } + ) + } + } + } + } +} + +@Composable +fun ModeButton( + text: String, + modifier: Modifier, + height: androidx.compose.ui.unit.Dp, + brush: Brush, + selected: Boolean, + onClick: () -> Unit +) { + val shape = RoundedCornerShape(24.dp) + val interaction = remember { MutableInteractionSource() } + var pressed by remember { mutableStateOf(false) } + + val scale by androidx.compose.animation.core.animateFloatAsState( + targetValue = if (pressed) 0.96f else 1f, + animationSpec = androidx.compose.animation.core.spring( + dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy, + stiffness = androidx.compose.animation.core.Spring.StiffnessLow + ), + label = "scale" + ) + + val elevation by androidx.compose.animation.core.animateDpAsState( + targetValue = if (selected) 16.dp else if (pressed) 4.dp else 8.dp, + animationSpec = androidx.compose.animation.core.tween(300), + label = "elevation" + ) + + val view = LocalView.current + + LaunchedEffect(interaction) { + interaction.interactions.collect { i -> + when (i) { + is PressInteraction.Press -> pressed = true + is PressInteraction.Release, is PressInteraction.Cancel -> pressed = false + } + } + } + + Box( + modifier = modifier + .height(height) + .scale(scale) + .shadow(elevation, shape) + .background( + if (selected) { + brush + } else { + Brush.linearGradient( + listOf( + Color(0x40FFFFFF), + Color(0x20FFFFFF) + ) + ) + }, + shape = shape + ) + .border( + width = if (selected) 2.dp else 1.dp, + brush = if (selected) { + Brush.linearGradient( + listOf( + Color(0x99FFFFFF), + Color(0x66FFFFFF) + ) + ) + } else { + Brush.linearGradient( + listOf( + Color(0x33FFFFFF), + Color(0x11FFFFFF) + ) + ) + }, + shape = shape + ) + .clickable(indication = null, interactionSource = interaction) { + view.playSoundEffect(SoundEffectConstants.CLICK) + onClick() + } + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = if (selected) Color.White else Color(0xFFB0B0B0), + fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium, + fontSize = if (selected) 22.sp else 20.sp, + style = MaterialTheme.typography.titleLarge + ) + } +} + +@Composable +fun LightRow(modifier: Modifier = Modifier) { + val lights = listOf( + "灯光 1" to 0.6f, + "灯光 2" to 0.8f, + "灯光 3" to 0.45f, + "灯光 4" to 0.6f, + "灯光 5" to 0.6f + ).take(2) + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) { + lights.forEach { (name, percent) -> + LightCard(name = name, percent = percent, modifier = Modifier.weight(1f)) + } + } +} + +@Composable +fun LightCard(name: String, percent: Float, modifier: Modifier = Modifier) { + Box(modifier = modifier.height(100.dp).clip(RoundedCornerShape(16.dp)).background(Color(0x26121212)).padding(12.dp)) { + Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x22121212))) + Column { Text(text = name); Text(text = "${(percent * 100).toInt()}%", color = Color(0xFF9AA0A6)) } + } + DotsProgress(percent) + } + } +} + + +@Composable +fun AddRoomDialog( + onDismiss: () -> Unit, + onConfirm: (String) -> Unit +) { + var roomName by remember { mutableStateOf("") } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x80000000)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onDismiss() }, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .width(400.dp) + .clip(RoundedCornerShape(24.dp)) + .background(Color(0xFF1E1E1E)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { /* 阻止点击穿透 */ } + .padding(24.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Text( + text = "添加房间", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + androidx.compose.material3.TextField( + value = roomName, + onValueChange = { roomName = it }, + placeholder = { Text("请输入房间名称", color = Color(0xFF9AA0A6)) }, + modifier = Modifier.fillMaxWidth(), + colors = androidx.compose.material3.TextFieldDefaults.colors( + focusedContainerColor = Color(0x22FFFFFF), + unfocusedContainerColor = Color(0x22FFFFFF), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + cursorColor = Color(0xFFA9F0FF), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp), + singleLine = true + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .weight(1f) + .height(48.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0x22FFFFFF)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onDismiss() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "取消", + color = Color(0xFFB0B0B0), + fontWeight = FontWeight.Medium + ) + } + + Box( + modifier = Modifier + .weight(1f) + .height(48.dp) + .clip(RoundedCornerShape(12.dp)) + .background( + Brush.linearGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + enabled = roomName.isNotBlank() + ) { + if (roomName.isNotBlank()) { + onConfirm(roomName.trim()) + } + } + .alpha(if (roomName.isNotBlank()) 1f else 0.5f), + contentAlignment = Alignment.Center + ) { + Text( + text = "确定", + color = Color.Black, + fontWeight = FontWeight.Bold + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/NeumorphButton.kt b/app/src/main/java/com/example/smarthome/ui/NeumorphButton.kt new file mode 100644 index 0000000..633f0a5 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/NeumorphButton.kt @@ -0,0 +1,57 @@ +package com.example.smarthome.ui + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.unit.dp + +@Composable +fun NeumorphButton(active: Boolean, onToggle: () -> Unit, content: @Composable () -> Unit) { + val shape = RoundedCornerShape(24.dp) + val scale by animateFloatAsState(targetValue = if (active) 0.98f else 1f, animationSpec = spring(), label = "scale") + Row( + modifier = Modifier + .scale(scale) + .background( + brush = Brush.linearGradient( + colors = listOf(Color(0xFF161616), Color(0xFF0E0E0E)) + ), + shape = shape + ) + .drawWithCache { + onDrawWithContent { + drawContent() + drawRect( + brush = Brush.radialGradient(listOf(Color(0x227B61FF), Color.Transparent)), + size = size + ) + } + } + .clickable { onToggle() } + .padding(PaddingValues(20.dp)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + content() + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/NeumorphControls.kt b/app/src/main/java/com/example/smarthome/ui/NeumorphControls.kt new file mode 100644 index 0000000..4770a5a --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/NeumorphControls.kt @@ -0,0 +1,46 @@ +package com.example.smarthome.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Column +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.painterResource +import androidx.compose.foundation.Image +import com.example.smarthome.R +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue + +@Composable +fun NeumorphControls() { + Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.fillMaxWidth()) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) { + ControlButton(iconRes = R.drawable.ic_light, label = "灯光") + ControlButton(iconRes = R.drawable.ic_ac, label = "空调") + ControlButton(iconRes = R.drawable.ic_curtain, label = "窗帘") + } + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) { + ControlButton(iconRes = R.drawable.ic_security, label = "安防") + ControlButton(iconRes = R.drawable.ic_media, label = "媒体") + } + } +} + +@Composable +fun ControlButton(iconRes: Int, label: String) { + var active by remember { mutableStateOf(false) } + NeumorphButton(active = active, onToggle = { active = !active }) { + Image(painter = painterResource(id = iconRes), contentDescription = null, modifier = Modifier.size(28.dp)) + Text(text = label) + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/Previews.kt b/app/src/main/java/com/example/smarthome/ui/Previews.kt new file mode 100644 index 0000000..d7a5c73 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/Previews.kt @@ -0,0 +1,25 @@ +package com.example.smarthome.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.example.smarthome.ui.theme.SmartHomeTheme + +@Preview(widthDp = 360, heightDp = 640, name = "16:9 Phone") +@Composable +fun Preview169() { SmartHomeTheme { MainScaffold(selectedRoom = 0, onRoomSelect = {}, selectedNavItem = 0, onNavItemSelect = {}, rooms = listOf("总览", "客厅"), onAddRoom = {}, onDeleteRoom = {}) } } + +@Preview(widthDp = 360, heightDp = 720, name = "18:9 Phone") +@Composable +fun Preview189() { SmartHomeTheme { MainScaffold(selectedRoom = 0, onRoomSelect = {}, selectedNavItem = 0, onNavItemSelect = {}, rooms = listOf("总览", "客厅"), onAddRoom = {}, onDeleteRoom = {}) } } + +@Preview(widthDp = 360, heightDp = 780, name = "19.5:9 Phone") +@Composable +fun Preview1959() { SmartHomeTheme { MainScaffold(selectedRoom = 0, onRoomSelect = {}, selectedNavItem = 0, onNavItemSelect = {}, rooms = listOf("总览", "客厅"), onAddRoom = {}, onDeleteRoom = {}) } } + +@Preview(widthDp = 1024, heightDp = 768, name = "4:3 Tablet") +@Composable +fun Preview43() { SmartHomeTheme { MainScaffold(selectedRoom = 0, onRoomSelect = {}, selectedNavItem = 0, onNavItemSelect = {}, rooms = listOf("总览", "客厅"), onAddRoom = {}, onDeleteRoom = {}) } } + +@Preview(widthDp = 900, heightDp = 600, name = "3:2 Tablet") +@Composable +fun Preview32() { SmartHomeTheme { MainScaffold(selectedRoom = 0, onRoomSelect = {}, selectedNavItem = 0, onNavItemSelect = {}, rooms = listOf("总览", "客厅"), onAddRoom = {}, onDeleteRoom = {}) } } diff --git a/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt b/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt new file mode 100644 index 0000000..74763a4 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt @@ -0,0 +1,357 @@ +package com.example.smarthome.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +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.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun SettingsScreen(onBack: () -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF121212)) + .padding(16.dp) + ) { + // 顶部栏 + SettingsTopBar(onBack = onBack) + + Spacer(modifier = Modifier.height(24.dp)) + + // 设置内容 + SettingsContentList() + } +} + +@Composable +fun SettingsContentList() { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { SettingsSection(title = "账户设置") { + SettingsItem( + title = "个人信息", + subtitle = "管理您的个人资料", + onClick = { } + ) + SettingsItem( + title = "账户安全", + subtitle = "密码和安全设置", + onClick = { } + ) + }} + + item { SettingsSection(title = "设备管理") { + var autoConnect by remember { mutableStateOf(true) } + SettingsSwitchItem( + title = "自动连接", + subtitle = "自动连接到家庭网络", + checked = autoConnect, + onCheckedChange = { autoConnect = it } + ) + + var notifications by remember { mutableStateOf(true) } + SettingsSwitchItem( + title = "设备通知", + subtitle = "接收设备状态通知", + checked = notifications, + onCheckedChange = { notifications = it } + ) + }} + + item { SettingsSection(title = "显示设置") { + var darkMode by remember { mutableStateOf(true) } + SettingsSwitchItem( + title = "深色模式", + subtitle = "使用深色主题", + checked = darkMode, + onCheckedChange = { darkMode = it } + ) + + SettingsItem( + title = "语言", + subtitle = "简体中文", + onClick = { } + ) + }} + + item { SettingsSection(title = "关于") { + SettingsItem( + title = "版本信息", + subtitle = "v1.0.0", + onClick = { } + ) + SettingsItem( + title = "隐私政策", + subtitle = "查看隐私政策", + onClick = { } + ) + SettingsItem( + title = "用户协议", + subtitle = "查看用户协议", + onClick = { } + ) + }} + + item { + Spacer(modifier = Modifier.height(16.dp)) + LogoutButton(onClick = { }) + } + } +} + +@Composable +fun SettingsTopBar(onBack: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0x22FFFFFF)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onBack() }, + contentAlignment = Alignment.Center + ) { + Text(text = "←", color = Color.White, fontSize = 20.sp) + } + Text( + text = "设置", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } +} + +@Composable +fun SettingsSection(title: String, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = title, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFF9AA0A6), + modifier = Modifier.padding(horizontal = 4.dp) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(Color(0x26FFFFFF)) + .padding(8.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + content() + } + } + } +} + +@Composable +fun SettingsItem( + title: String, + subtitle: String, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color(0x11FFFFFF)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onClick() } + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = title, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = Color.White + ) + Text( + text = subtitle, + fontSize = 13.sp, + color = Color(0xFF9AA0A6) + ) + } + Text( + text = "›", + fontSize = 24.sp, + color = Color(0xFF9AA0A6) + ) + } + } +} + +@Composable +fun SettingsSwitchItem( + title: String, + subtitle: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color(0x11FFFFFF)) + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = title, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = Color.White + ) + Text( + text = subtitle, + fontSize = 13.sp, + color = Color(0xFF9AA0A6) + ) + } + + CustomSwitch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } + } +} + +@Composable +fun CustomSwitch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Box( + modifier = Modifier + .size(52.dp, 30.dp) + .clip(RoundedCornerShape(15.dp)) + .background( + if (checked) { + Brush.linearGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ) + } else { + Brush.linearGradient( + listOf(Color(0x33222222), Color(0x33222222)) + ) + } + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onCheckedChange(!checked) } + .padding(3.dp) + ) { + Box( + modifier = Modifier + .size(24.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + .align(if (checked) Alignment.CenterEnd else Alignment.CenterStart) + ) + } +} + +@Composable +fun LogoutButton(onClick: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .clip(RoundedCornerShape(24.dp)) + .background( + Brush.linearGradient( + listOf(Color(0xFFFF6B6B), Color(0xFFFF5252)) + ) + ) + .shadow(8.dp, RoundedCornerShape(24.dp), clip = false) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onClick() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "退出登录", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } +} + + +@Composable +fun SettingsContent() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // 顶部标题 + Text( + text = "设置", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 24.dp) + ) + + // 设置内容 + SettingsContentList() + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/theme/Theme.kt b/app/src/main/java/com/example/smarthome/ui/theme/Theme.kt new file mode 100644 index 0000000..c917f12 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/theme/Theme.kt @@ -0,0 +1,24 @@ +package com.example.smarthome.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val DarkColors = darkColorScheme( + primary = Color(0xFF121212), + secondary = Color(0xFF7B61FF), + background = Color(0xFF121212), + surface = Color(0xFF121212), + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color.White, + onSurface = Color.White +) + +@Composable +fun SmartHomeTheme(content: @Composable () -> Unit) { + MaterialTheme(colorScheme = DarkColors) { + content() + } +} diff --git a/app/src/main/res/drawable/bg_radial_overlay.xml b/app/src/main/res/drawable/bg_radial_overlay.xml new file mode 100644 index 0000000..1f9ae55 --- /dev/null +++ b/app/src/main/res/drawable/bg_radial_overlay.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ac.xml b/app/src/main/res/drawable/ic_ac.xml new file mode 100644 index 0000000..08b5cfa --- /dev/null +++ b/app/src/main/res/drawable/ic_ac.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bookmark.xml b/app/src/main/res/drawable/ic_bookmark.xml new file mode 100644 index 0000000..05282ea --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_curtain.xml b/app/src/main/res/drawable/ic_curtain.xml new file mode 100644 index 0000000..f1dd4fa --- /dev/null +++ b/app/src/main/res/drawable/ic_curtain.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_dashboard.xml b/app/src/main/res/drawable/ic_dashboard.xml new file mode 100644 index 0000000..7b9391c --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000..a53cc37 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,9 @@ + + + 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..318eab8 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..66a9344 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_light.xml b/app/src/main/res/drawable/ic_light.xml new file mode 100644 index 0000000..92c766e --- /dev/null +++ b/app/src/main/res/drawable/ic_light.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_media.xml b/app/src/main/res/drawable/ic_media.xml new file mode 100644 index 0000000..b5f30e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_media.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_recent.xml b/app/src/main/res/drawable/ic_recent.xml new file mode 100644 index 0000000..dc387ba --- /dev/null +++ b/app/src/main/res/drawable/ic_recent.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_room1.xml b/app/src/main/res/drawable/ic_room1.xml new file mode 100644 index 0000000..5c7dc69 --- /dev/null +++ b/app/src/main/res/drawable/ic_room1.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_room2.xml b/app/src/main/res/drawable/ic_room2.xml new file mode 100644 index 0000000..8d0a4e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_room2.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_room3.xml b/app/src/main/res/drawable/ic_room3.xml new file mode 100644 index 0000000..dab14b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_room3.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_room4.xml b/app/src/main/res/drawable/ic_room4.xml new file mode 100644 index 0000000..fdc716c --- /dev/null +++ b/app/src/main/res/drawable/ic_room4.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_room5.xml b/app/src/main/res/drawable/ic_room5.xml new file mode 100644 index 0000000..33c7118 --- /dev/null +++ b/app/src/main/res/drawable/ic_room5.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_security.xml b/app/src/main/res/drawable/ic_security.xml new file mode 100644 index 0000000..6ada778 --- /dev/null +++ b/app/src/main/res/drawable/ic_security.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..420b550 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_support.xml b/app/src/main/res/drawable/ic_support.xml new file mode 100644 index 0000000..5bbbc6f --- /dev/null +++ b/app/src/main/res/drawable/ic_support.xml @@ -0,0 +1,9 @@ + + + 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..624d3b2 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..d9f94e6 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/raw/design_tokens.json b/app/src/main/res/raw/design_tokens.json new file mode 100644 index 0000000..da9e004 --- /dev/null +++ b/app/src/main/res/raw/design_tokens.json @@ -0,0 +1,34 @@ +{ + "grid": { + "unit": 8, + "spacing": [4, 8, 16, 24, 32, 40, 48] + }, + "colors": { + "primary": "#121212", + "accent": "#7B61FF", + "glassOverlay": "#1A121212", + "gradientStart": "#6A3DFF", + "gradientEnd": "#3A0CA3" + }, + "radius": { + "xs": 8, + "sm": 12, + "md": 16, + "lg": 24 + }, + "shadow": { + "x": 8, + "y": 8, + "blur": 16 + }, + "animation": { + "durationMin": 300, + "durationMax": 450, + "fps": 60 + }, + "icons": { + "strokeWidth": 2, + "glowColor": "#7B61FF", + "glowBlurPt": 12 + } +} diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..73cd3f0 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + #121212 + #7B61FF + #7B61FF + #6A3DFF + #3A0CA3 + #1A121212 + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..9397267 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ace0bbe --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("com.android.application") version "8.6.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.24" apply false +} + +tasks.register("clean", Delete::class) { + delete(rootProject.layout.buildDirectory) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ca4847e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.enableJetifier=true +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..980502d167d3610f88fa03b2f717935189d9fbcf GIT binary patch literal 43739 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 z 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2893fda --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven(url = "https://jitpack.io") + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven(url = "https://jitpack.io") + } +} + +rootProject.name = "SmartHome" +include(":app")