commit e606ed6f0bad1d59874629ba239f0e90f1266cee Author: zzh Date: Wed Nov 26 10:30:03 2025 +0800 初始提交:智能家居控制台应用 功能特性: - 玻璃态/新拟态设计风格的现代UI - 侧边栏导航(控制台、最近、收藏、下载、支持、设置) - 房间管理:可添加、删除房间,支持横向滚动 - 长按房间标签进入编辑模式,带旋转晃动动画 - 三个场景模式按钮(回家、出门、玩乐) - 空调控制卡片 - 使用状态图表 - 灯光控制 - 设备网格展示 - 设置页面(账户、设备管理、显示设置、关于) - 使用SharedPreferences持久化存储房间列表 - 响应式设计,支持多种屏幕尺寸 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 0000000..980502d Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..128196a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$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")