mirror of
http://180.163.74.83:13000/zhangzhenghao/MPVN_Android.git
synced 2025-12-12 15:24:30 +00:00
初始提交:智能家居控制台应用
功能特性: - 玻璃态/新拟态设计风格的现代UI - 侧边栏导航(控制台、最近、收藏、下载、支持、设置) - 房间管理:可添加、删除房间,支持横向滚动 - 长按房间标签进入编辑模式,带旋转晃动动画 - 三个场景模式按钮(回家、出门、玩乐) - 空调控制卡片 - 使用状态图表 - 灯光控制 - 设备网格展示 - 设置页面(账户、设备管理、显示设置、关于) - 使用SharedPreferences持久化存储房间列表 - 响应式设计,支持多种屏幕尺寸
This commit is contained in:
commit
e606ed6f0b
81
.gitignore
vendored
Normal file
81
.gitignore
vendored
Normal file
@ -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
|
||||
80
app/build.gradle.kts
Normal file
80
app/build.gradle.kts
Normal file
@ -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")
|
||||
}
|
||||
1
app/proguard-rules.pro
vendored
Normal file
1
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1 @@
|
||||
# Keep LeakCanary in debug only; no rules needed for release here
|
||||
@ -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<MainActivity>()
|
||||
|
||||
@Test
|
||||
fun continuousClickTest() {
|
||||
repeat(1000) {
|
||||
composeRule.onNodeWithText("灯光").performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<MainActivity>()
|
||||
|
||||
@Test
|
||||
fun dpiAdaptation160to640() {
|
||||
listOf(160f, 240f, 320f, 480f, 640f).forEach { dpi ->
|
||||
rule.setContent {
|
||||
CompositionLocalProvider(LocalDensity provides Density(dpi / 160f)) {
|
||||
ControlPanel(roomIndex = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/src/main/AndroidManifest.xml
Normal file
15
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:name=".SmartHomeApp"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="SmartHome"
|
||||
android:theme="@style/Theme.SmartHome">
|
||||
<activity android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
102
app/src/main/java/com/example/smarthome/MainActivity.kt
Normal file
102
app/src/main/java/com/example/smarthome/MainActivity.kt
Normal file
@ -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<ComposeView>(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<BlurTarget>(R.id.blurTarget)
|
||||
val blurView = findViewById<BlurView>(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<String>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
app/src/main/java/com/example/smarthome/SmartHomeApp.kt
Normal file
5
app/src/main/java/com/example/smarthome/SmartHomeApp.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package com.example.smarthome
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class SmartHomeApp : Application()
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
908
app/src/main/java/com/example/smarthome/ui/MainScaffold.kt
Normal file
908
app/src/main/java/com/example/smarthome/ui/MainScaffold.kt
Normal file
@ -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<String> = 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<String> = 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<String> = 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<Mode?>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/src/main/java/com/example/smarthome/ui/NeumorphButton.kt
Normal file
57
app/src/main/java/com/example/smarthome/ui/NeumorphButton.kt
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
25
app/src/main/java/com/example/smarthome/ui/Previews.kt
Normal file
25
app/src/main/java/com/example/smarthome/ui/Previews.kt
Normal file
@ -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 = {}) } }
|
||||
357
app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt
Normal file
357
app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
24
app/src/main/java/com/example/smarthome/ui/theme/Theme.kt
Normal file
24
app/src/main/java/com/example/smarthome/ui/theme/Theme.kt
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
14
app/src/main/res/drawable/bg_radial_overlay.xml
Normal file
14
app/src/main/res/drawable/bg_radial_overlay.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#121212"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<gradient android:type="radial" android:gradientRadius="800dp"
|
||||
android:startColor="#6A3DFF" android:endColor="#3A0CA3"/>
|
||||
<size android:width="1080dp" android:height="1920dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
4
app/src/main/res/drawable/ic_ac.xml
Normal file
4
app/src/main/res/drawable/ic_ac.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M3,8 L21,8 L21,12 L3,12 Z"/>
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M5,16 L19,16"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_bookmark.xml
Normal file
9
app/src/main/res/drawable/ic_bookmark.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_curtain.xml
Normal file
5
app/src/main/res/drawable/ic_curtain.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M3,4 L21,4"/>
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M3,5 L3,20"/>
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M21,5 L21,20"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_dashboard.xml
Normal file
9
app/src/main/res/drawable/ic_dashboard.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_download.xml
Normal file
9
app/src/main/res/drawable/ic_download.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||
</vector>
|
||||
3
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
3
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#121212"/>
|
||||
</shape>
|
||||
4
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
4
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:pathData="M24,54 L84,54" android:strokeColor="#FFFFFFFF" android:strokeWidth="6" android:fillColor="#00000000"/>
|
||||
<path android:pathData="M54,24 L54,84" android:strokeColor="#FFFFFFFF" android:strokeWidth="6" android:fillColor="#00000000"/>
|
||||
</vector>
|
||||
4
app/src/main/res/drawable/ic_light.xml
Normal file
4
app/src/main/res/drawable/ic_light.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M12,3 L15,10 L9,10 Z"/>
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M10,14 L14,14 L14,21 L10,21 Z"/>
|
||||
</vector>
|
||||
4
app/src/main/res/drawable/ic_media.xml
Normal file
4
app/src/main/res/drawable/ic_media.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M4,6 L20,6 L20,18 L4,18 Z"/>
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M10,10 L14,12 L10,14 Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_recent.xml
Normal file
9
app/src/main/res/drawable/ic_recent.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||
</vector>
|
||||
3
app/src/main/res/drawable/ic_room1.xml
Normal file
3
app/src/main/res/drawable/ic_room1.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M3,8 L21,8 L21,21 L3,21 Z"/>
|
||||
</vector>
|
||||
3
app/src/main/res/drawable/ic_room2.xml
Normal file
3
app/src/main/res/drawable/ic_room2.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M3,6 L21,6 L21,21 L3,21 Z"/>
|
||||
</vector>
|
||||
3
app/src/main/res/drawable/ic_room3.xml
Normal file
3
app/src/main/res/drawable/ic_room3.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M4,7 L20,7 L20,20 L4,20 Z"/>
|
||||
</vector>
|
||||
3
app/src/main/res/drawable/ic_room4.xml
Normal file
3
app/src/main/res/drawable/ic_room4.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M5,8 L19,8 L19,19 L5,19 Z"/>
|
||||
</vector>
|
||||
3
app/src/main/res/drawable/ic_room5.xml
Normal file
3
app/src/main/res/drawable/ic_room5.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M6,9 L18,9 L18,18 L6,18 Z"/>
|
||||
</vector>
|
||||
3
app/src/main/res/drawable/ic_security.xml
Normal file
3
app/src/main/res/drawable/ic_security.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M12,3 L21,8 L12,21 L3,8 Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_settings.xml
Normal file
9
app/src/main/res/drawable/ic_settings.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94 0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_support.xml
Normal file
9
app/src/main/res/drawable/ic_support.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
</vector>
|
||||
27
app/src/main/res/layout/activity_main.xml
Normal file
27
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_radial_overlay"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<eightbitlab.com.blurview.BlurTarget
|
||||
android:id="@+id/blurTarget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/composeView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</eightbitlab.com.blurview.BlurTarget>
|
||||
|
||||
<eightbitlab.com.blurview.BlurView
|
||||
android:id="@+id/blurView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
app:blurOverlayColor="@color/glassOverlay" />
|
||||
|
||||
</FrameLayout>
|
||||
4
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
4
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
34
app/src/main/res/raw/design_tokens.json
Normal file
34
app/src/main/res/raw/design_tokens.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
8
app/src/main/res/values/colors.xml
Normal file
8
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
<color name="colorPrimary">#121212</color>
|
||||
<color name="colorAccent">#7B61FF</color>
|
||||
<color name="glowAccent">#7B61FF</color>
|
||||
<color name="gradientStart">#6A3DFF</color>
|
||||
<color name="gradientEnd">#3A0CA3</color>
|
||||
<color name="glassOverlay">#1A121212</color>
|
||||
</resources>
|
||||
9
app/src/main/res/values/themes.xml
Normal file
9
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.SmartHome" parent="Theme.Material3.Dark.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimary</item>
|
||||
<item name="android:windowBackground">@color/colorPrimary</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimary</item>
|
||||
<item name="android:forceDarkAllowed">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
8
build.gradle.kts
Normal file
8
build.gradle.kts
Normal file
@ -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)
|
||||
}
|
||||
4
gradle.properties
Normal file
4
gradle.properties
Normal file
@ -0,0 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin.code.style=official
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -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
|
||||
251
gradlew
vendored
Executable file
251
gradlew
vendored
Executable file
@ -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" "$@"
|
||||
94
gradlew.bat
vendored
Normal file
94
gradlew.bat
vendored
Normal file
@ -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
|
||||
20
settings.gradle.kts
Normal file
20
settings.gradle.kts
Normal file
@ -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")
|
||||
Loading…
Reference in New Issue
Block a user