diff --git a/app/src/main/java/com/example/smarthome/MainActivity.kt b/app/src/main/java/com/example/smarthome/MainActivity.kt index c2793aa..50780ad 100644 --- a/app/src/main/java/com/example/smarthome/MainActivity.kt +++ b/app/src/main/java/com/example/smarthome/MainActivity.kt @@ -28,11 +28,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.collectAsState import com.example.smarthome.data.BackgroundManager +import com.example.smarthome.data.LanguageManager class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // 初始化语言管理器 + LanguageManager.init(this) + // 隐藏状态栏和导航栏 hideStatusBar() diff --git a/app/src/main/java/com/example/smarthome/data/BackgroundManager.kt b/app/src/main/java/com/example/smarthome/data/BackgroundManager.kt new file mode 100644 index 0000000..151c51e --- /dev/null +++ b/app/src/main/java/com/example/smarthome/data/BackgroundManager.kt @@ -0,0 +1,88 @@ +package com.example.smarthome.data + +import android.content.Context +import android.content.SharedPreferences +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.example.smarthome.R +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * 背景壁纸管理器 + */ +object BackgroundManager { + private const val PREFS_NAME = "background_prefs" + private const val KEY_BACKGROUND = "selected_background" + + private val _selectedBackground = MutableStateFlow(0) + val selectedBackground: StateFlow = _selectedBackground.asStateFlow() + + // 可用的背景列表 + val backgrounds = listOf( + BackgroundOption( + id = 0, + name = "丝绸白", + resourceId = R.drawable.background1, + thumbId = R.drawable.background1_thumb, + description = "优雅的白色丝绸质感", + isDark = false + ), + BackgroundOption( + id = 1, + name = "星空海", + resourceId = R.drawable.background2, + thumbId = R.drawable.background2_thumb, + description = "深邃的星空与荧光海浪", + isDark = true + ), + BackgroundOption( + id = 2, + name = "紫金绸", + resourceId = R.drawable.background3, + thumbId = R.drawable.background3_thumb, + description = "紫色金丝流动质感", + isDark = true + ) + ) + + fun init(context: Context) { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + _selectedBackground.value = prefs.getInt(KEY_BACKGROUND, 0) + } + + fun setBackground(context: Context, backgroundId: Int) { + _selectedBackground.value = backgroundId + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putInt(KEY_BACKGROUND, backgroundId) + .apply() + } + + fun getCurrentBackgroundResource(): Int { + val id = _selectedBackground.value + return backgrounds.getOrNull(id)?.resourceId ?: R.drawable.background1 + } +} + +data class BackgroundOption( + val id: Int, + val name: String, + val resourceId: Int, + val thumbId: Int, + val description: String, + val isDark: Boolean = false +) + +// Compose 便捷方法 +@Composable +fun rememberSelectedBackground(): Int { + val context = LocalContext.current + remember { BackgroundManager.init(context) } + val selected by BackgroundManager.selectedBackground.collectAsState() + return BackgroundManager.backgrounds.getOrNull(selected)?.resourceId ?: R.drawable.background1 +} diff --git a/app/src/main/java/com/example/smarthome/data/CanService.kt b/app/src/main/java/com/example/smarthome/data/CanService.kt new file mode 100644 index 0000000..8e3ace0 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/data/CanService.kt @@ -0,0 +1,476 @@ +package com.example.smarthome.data + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.LinkedList +import java.util.Queue + +/** + * CAN 总线服务 + * + * 依赖库(需要在 build.gradle 中添加): + * implementation("com.vi:vioserial:x.x.x") // 串口通信库 + * + * CAN 帧格式(与原代码一致): + * AA 01 00 08 00 00 XX XX [8字节数据] 7A + * - AA: 帧头 + * - 01 00: 固定 + * - 08: 数据长度(8字节) + * - 00 00 XX XX: CAN ID(格口地址) + * - [8字节]: 命令数据(ASCII HEX) + * - 7A: 帧尾 + */ + +// ==================== 常量定义 ==================== + +object CanConstants { + const val TAG = "CanService" + + // 默认串口配置 + const val DEFAULT_COM = "/dev/ttyS4" // 方派10寸PAD + // const val DEFAULT_COM = "/dev/ttysWK1" // 萤火虫10寸PAD + // const val DEFAULT_COM = "/dev/ttyS3" // 梵骏10寸PAD + + const val BAUD_RATE = 115200 + + // CAN 帧格式 + const val FRAME_HEAD = "AA0100" + const val FRAME_DATA_LEN = "08" + const val FRAME_TAIL = "7A" + + // 门锁状态(根据锁类型可能相反) + const val DOOR_OPEN_STATUS = "1" // 新锁:1=开 + const val DOOR_CLOSE_STATUS = "0" // 新锁:0=关 +} + +// ==================== CAN 命令定义 ==================== + +/** + * CAN 命令枚举(与原代码一致) + */ +object CanCommands { + // 门锁控制 + const val LON = "LON" // 开锁 + const val LOF = "LOF" // 关锁 + const val LST = "LST" // 查询锁状态 + + // 电源控制 + const val PON = "PON" // 上电 + const val POF = "POF" // 断电 + + // LED 控制 + const val RON = "RON" // 红灯亮 + const val GON = "GON" // 绿灯亮 + const val AOF = "AOF" // 灯灭 + + // 设备信息读取 + const val PVD = "PVD" // 读取 PID/VID + const val MFG = "MFG" // 读取制造商 + const val PRO = "PRO" // 读取产品名 + const val DSN = "DSN" // 读取设备序列号 + const val CON = "CON" // 读取连接状态 + + // 固件相关 + const val VER = "1VER" // 读取固件版本 + const val LEN = "1LEN" // 固件长度 + const val MD5 = "1MD5" // 固件MD5 + const val MDE = "1MDE" // 固件MD5结束 +} + +// ==================== 数据模型 ==================== + +/** + * CAN 消息 + */ +data class CanMessage( + val cellName: String, // 格口名称,如 "A01" + val command: String, // 命令,如 "LON" + val data: String = "", // 附加数据 + val rawFrame: String = "" // 原始帧数据 +) + +/** + * CAN 响应 + */ +data class CanResponse( + val cellName: String, + val command: String, + val value: String, + val rawData: String +) + +// ==================== CAN 服务接口 ==================== + +interface ICanService { + val isConnected: StateFlow + val lastResponse: StateFlow + + // 串口管理 + fun openCom(comPath: String = CanConstants.DEFAULT_COM, baudRate: Int = CanConstants.BAUD_RATE): Boolean + fun closeCom() + + // 发送命令 + fun sendCmd(cellName: String, cmd: String) + fun sendData(addr: String, cmd: String, data: String) + + // 门锁控制 + fun openDoor(cellName: String) + fun closeDoor(cellName: String) + fun queryDoorStatus(cellName: String) + + // 电源控制 + fun powerOn(cellName: String) + fun powerOff(cellName: String) + + // LED 控制 + fun ledRed(cellName: String) + fun ledGreen(cellName: String) + fun ledOff(cellName: String) + + // 设备信息 + fun readDeviceSn(cellName: String) + fun readDevicePvd(cellName: String) + fun readDeviceMfg(cellName: String) + fun readDevicePro(cellName: String) + fun readConnectStatus(cellName: String) + + // 批量操作 + fun openDoors(cellNames: List) + fun powerOnAll(cellNames: List) + fun powerOffAll(cellNames: List) + fun ledGreenAll(cellNames: List) + fun ledOffAll(cellNames: List) + + // 回调 + fun setResponseCallback(callback: (CanResponse) -> Unit) +} + +// ==================== CAN 服务实现(模拟)==================== + +class CanServiceImpl private constructor( + private val context: Context +) : ICanService { + + companion object { + @Volatile + private var instance: CanServiceImpl? = null + + fun getInstance(context: Context): CanServiceImpl { + return instance ?: synchronized(this) { + instance ?: CanServiceImpl(context.applicationContext).also { instance = it } + } + } + } + + // 状态 + private val _isConnected = MutableStateFlow(false) + override val isConnected: StateFlow = _isConnected.asStateFlow() + + private val _lastResponse = MutableStateFlow(null) + override val lastResponse: StateFlow = _lastResponse.asStateFlow() + + // 发送队列 + private val sendQueue: Queue = LinkedList() + + // 回调 + private var responseCallback: ((CanResponse) -> Unit)? = null + + // TODO: 真实串口对象 + // private var comSerial: COMSerial? = null + + // ==================== 串口管理 ==================== + + override fun openCom(comPath: String, baudRate: Int): Boolean { + Log.d(CanConstants.TAG, "openCom: $comPath, baudRate: $baudRate") + + // TODO: 打开真实串口 + // comSerial = COMSerial.instance() + // val result = comSerial?.open(comPath, baudRate) ?: false + // if (result) { + // comSerial?.setOnDataListener(onComDataListener) + // _isConnected.value = true + // startSendThread() + // startReceiveThread() + // } + // return result + + // 模拟连接成功 + _isConnected.value = true + return true + } + + override fun closeCom() { + Log.d(CanConstants.TAG, "closeCom") + _isConnected.value = false + + // TODO: 关闭真实串口 + // comSerial?.close() + // comSerial = null + } + + // ==================== 发送命令 ==================== + + override fun sendCmd(cellName: String, cmd: String) { + val addr = convertCellNameToCanId(cellName) + val convertCmd = stringToHexAscii(cmd.padEnd(7, ':').take(7)) + + val canPackage = buildCanFrame(addr, convertCmd) + sendPortData(canPackage, "$cellName$cmd") + } + + override fun sendData(addr: String, cmd: String, data: String) { + val convertCmd = when { + cmd == CanCommands.VER -> stringToHexAscii(cmd.padEnd(8, ':')) + cmd in listOf(CanCommands.LEN, CanCommands.MD5, CanCommands.MDE) -> + stringToHexAscii(cmd) + data + cmd.isEmpty() -> data.padEnd(16, ':'.code.toByte().toInt().toChar()) + else -> stringToHexAscii(cmd.padEnd(8, ':')) + } + + val canPackage = buildCanFrame("0000$addr", convertCmd) + sendPortData(canPackage, "$addr$cmd") + } + + // ==================== 门锁控制 ==================== + + override fun openDoor(cellName: String) { + sendCmd(cellName, CanCommands.LON) + } + + override fun closeDoor(cellName: String) { + sendCmd(cellName, CanCommands.LOF) + } + + override fun queryDoorStatus(cellName: String) { + sendCmd(cellName, CanCommands.LST) + } + + // ==================== 电源控制 ==================== + + override fun powerOn(cellName: String) { + sendCmd(cellName, CanCommands.PON) + } + + override fun powerOff(cellName: String) { + sendCmd(cellName, CanCommands.POF) + } + + // ==================== LED 控制 ==================== + + override fun ledRed(cellName: String) { + sendCmd(cellName, CanCommands.RON) + } + + override fun ledGreen(cellName: String) { + sendCmd(cellName, CanCommands.GON) + } + + override fun ledOff(cellName: String) { + sendCmd(cellName, CanCommands.AOF) + } + + // ==================== 设备信息 ==================== + + override fun readDeviceSn(cellName: String) { + sendCmd(cellName, CanCommands.DSN) + } + + override fun readDevicePvd(cellName: String) { + sendCmd(cellName, CanCommands.PVD) + } + + override fun readDeviceMfg(cellName: String) { + sendCmd(cellName, CanCommands.MFG) + } + + override fun readDevicePro(cellName: String) { + sendCmd(cellName, CanCommands.PRO) + } + + override fun readConnectStatus(cellName: String) { + sendCmd(cellName, CanCommands.CON) + } + + // ==================== 批量操作 ==================== + + override fun openDoors(cellNames: List) { + cellNames.forEach { openDoor(it) } + } + + override fun powerOnAll(cellNames: List) { + cellNames.forEach { powerOn(it) } + } + + override fun powerOffAll(cellNames: List) { + cellNames.forEach { powerOff(it) } + } + + override fun ledGreenAll(cellNames: List) { + cellNames.forEach { ledGreen(it) } + } + + override fun ledOffAll(cellNames: List) { + cellNames.forEach { ledOff(it) } + } + + // ==================== 回调 ==================== + + override fun setResponseCallback(callback: (CanResponse) -> Unit) { + responseCallback = callback + } + + // ==================== 内部方法 ==================== + + private fun sendPortData(frame: String, info: String) { + if (!_isConnected.value) { + Log.w(CanConstants.TAG, "sendPortData: 串口未打开") + return + } + + Log.d(CanConstants.TAG, "sendPortData: $info -> $frame") + sendQueue.add("$frame$info") + + // TODO: 真实发送 + // comSerial?.send(hexStringToBytes(frame)) + } + + /** + * 构建 CAN 帧 + */ + private fun buildCanFrame(canId: String, data: String): String { + return "${CanConstants.FRAME_HEAD}${CanConstants.FRAME_DATA_LEN}$canId$data${CanConstants.FRAME_TAIL}" + } + + /** + * 格口名称转 CAN ID + * 例如: "A01" -> "0101", "B02" -> "0202" + */ + private fun convertCellNameToCanId(cellName: String): String { + if (cellName.length < 3) return "0000" + + val cabinet = cellName[0] // A, B, C... + val cell = cellName.substring(1).toIntOrNull() ?: 0 + + val cabinetId = (cabinet.uppercaseChar() - 'A' + 1).toString().padStart(2, '0') + val cellId = cell.toString().padStart(2, '0') + + return "$cabinetId$cellId" + } + + /** + * CAN ID 转格口名称 + */ + private fun convertCanIdToCellName(canId: String): String { + if (canId.length < 4) return "" + + val cabinetNum = canId.substring(0, 2).toIntOrNull() ?: 0 + val cellNum = canId.substring(2, 4).toIntOrNull() ?: 0 + + val cabinet = ('A' + cabinetNum - 1) + return "$cabinet${cellNum.toString().padStart(2, '0')}" + } + + /** + * 字符串转 HEX ASCII + */ + private fun stringToHexAscii(str: String): String { + return str.map { Integer.toHexString(it.code).padStart(2, '0') }.joinToString("") + } + + /** + * HEX 字符串转字节数组 + */ + private fun hexStringToBytes(hex: String): ByteArray { + val len = hex.length / 2 + val result = ByteArray(len) + for (i in 0 until len) { + result[i] = hex.substring(i * 2, i * 2 + 2).toInt(16).toByte() + } + return result + } + + /** + * 处理接收到的数据(由串口回调调用) + */ + internal fun handleReceivedData(data: String) { + Log.d(CanConstants.TAG, "handleReceivedData: $data") + + // 解析 CAN 帧 + // 格式: AA0100 08 CANID DATA 7A + if (data.length < 26 || !data.startsWith("AA") || !data.endsWith("7A")) { + return + } + + try { + val canId = data.substring(10, 14) + val payload = data.substring(14, 30) + + val cellName = convertCanIdToCellName(canId) + val (cmd, value) = parsePayload(payload) + + val response = CanResponse( + cellName = cellName, + command = cmd, + value = value, + rawData = data + ) + + _lastResponse.value = response + responseCallback?.invoke(response) + + } catch (e: Exception) { + Log.e(CanConstants.TAG, "parseReceivedData error", e) + } + } + + /** + * 解析数据负载 + */ + private fun parsePayload(payload: String): Pair { + // 将 HEX 转回 ASCII + val ascii = StringBuilder() + for (i in payload.indices step 2) { + val byte = payload.substring(i, i + 2).toInt(16) + if (byte != 0x3A) { // 不是冒号 + ascii.append(byte.toChar()) + } + } + + val str = ascii.toString() + val cmd = str.take(3) + val value = str.drop(3).trim() + + return cmd to value + } +} + +// ==================== 便捷扩展 ==================== + +fun Context.getCanService(): ICanService = CanServiceImpl.getInstance(this) + +// ==================== 操作类型常量(与原代码一致)==================== + +object OperationType { + const val OPT_BORROW = 2 // 借 + const val OPT_RETURN = 3 // 还 + const val OPT_REPAIRDEVIN = 4 // 报修归还 + const val OPT_REPAIRDEVOUT = 5 // 维修设备出柜 + const val OPT_BATCHOUT = 6 // 批量出柜 + const val OPT_BATCHIN = 7 // 批量新增入柜 + const val OPT_RESETSELECTECELLS = 8 // 恢复异常格口状态 + const val OPT_REPAIRSELECTECELLS = 9 // 格口报修 + const val OPT_PONSELECTECELLS = 10 // 指定格口上电 + const val OPT_POFSELECTECELLS = 11 // 指定格口断电 + const val OPT_RONSELECTECELLS = 12 // 指定格口红灯亮 + const val OPT_GONSELECTECELLS = 13 // 指定格口绿灯亮 + const val OPT_AOFSELECTECELLS = 14 // 指定格口灯灭 + const val OPT_DEVINFSELECTECELLS = 15 // 指定格口设备信息 + const val OPT_FIRMWAREVERSION = 16 // 主板固件版本 + const val OPT_OPENDOORS = 17 // 打开柜门 + const val OPT_REPAIRCELLOPEN = 18 // 报修格口开门 + const val OPT_LOSTCELLRON = 19 // 丢失格口红灯 + const val OPT_DOUBLECHECK = 20 // 检测打开门的柜子格口数据是否异常 +} diff --git a/app/src/main/java/com/example/smarthome/data/DeviceManager.kt b/app/src/main/java/com/example/smarthome/data/DeviceManager.kt new file mode 100644 index 0000000..27d64c3 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/data/DeviceManager.kt @@ -0,0 +1,406 @@ +package com.example.smarthome.data + +import android.content.Context +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * 设备管理器 - 整合 MQTT 和 CAN 服务 + * 提供给 UI 层使用的统一接口 + * + * 使用示例: + * ```kotlin + * @Composable + * fun DeviceControlScreen() { + * val deviceManager = rememberDeviceManager() + * + * // 初始化 + * LaunchedEffect(Unit) { + * deviceManager.initialize() + * } + * + * // 控制设备 + * Button(onClick = { deviceManager.openDoor("A01") }) { + * Text("开门") + * } + * } + * ``` + */ +class DeviceManager private constructor( + private val context: Context +) { + companion object { + private const val TAG = "DeviceManager" + + @Volatile + private var instance: DeviceManager? = null + + fun getInstance(context: Context): DeviceManager { + return instance ?: synchronized(this) { + instance ?: DeviceManager(context.applicationContext).also { instance = it } + } + } + } + + // 服务 + private val mqttService: IMqttService = MqttServiceImpl.getInstance(context) + private val canService: ICanService = CanServiceImpl.getInstance(context) + + // 协程作用域 + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + // 状态 + private val _isInitialized = MutableStateFlow(false) + val isInitialized: StateFlow = _isInitialized.asStateFlow() + + private val _deviceSn = MutableStateFlow("") + val deviceSn: StateFlow = _deviceSn.asStateFlow() + + // 暴露服务状态 + val mqttConnectionState = mqttService.connectionState + val canConnectionState = canService.isConnected + val lastCanResponse = canService.lastResponse + + // 当前操作 + private val _currentOperation = MutableStateFlow(-1) + val currentOperation: StateFlow = _currentOperation.asStateFlow() + + // 格口列表 + private val _openCellList = MutableStateFlow>(emptyList()) + val openCellList: StateFlow> = _openCellList.asStateFlow() + + // ==================== 初始化 ==================== + + /** + * 初始化设备管理器 + */ + fun initialize( + deviceSn: String = "", + mqttHost: String = "", + mqttPassword: String = "", + comPath: String = CanConstants.DEFAULT_COM + ) { + Log.d(TAG, "initialize") + + _deviceSn.value = deviceSn + + // 初始化 CAN 串口 + scope.launch { + val canResult = canService.openCom(comPath) + Log.d(TAG, "CAN openCom result: $canResult") + } + + // 初始化 MQTT(如果配置了) + if (mqttHost.isNotEmpty() && deviceSn.isNotEmpty()) { + scope.launch { + mqttService.mqttInit(mqttHost, deviceSn, mqttPassword) + mqttService.mqttConnect() + } + } + + // 设置回调 + setupCallbacks() + + _isInitialized.value = true + } + + /** + * 关闭设备管理器 + */ + fun shutdown() { + Log.d(TAG, "shutdown") + canService.closeCom() + mqttService.mqttClose() + _isInitialized.value = false + } + + private fun setupCallbacks() { + // CAN 响应回调 + canService.setResponseCallback { response -> + Log.d(TAG, "CAN Response: ${response.cellName} ${response.command} = ${response.value}") + handleCanResponse(response) + } + + // MQTT 消息回调 + mqttService.setMessageCallback { topic, message -> + Log.d(TAG, "MQTT Message: $topic -> $message") + handleMqttMessage(topic, message) + } + } + + // ==================== 门锁控制 ==================== + + /** + * 开门 + */ + fun openDoor(cellName: String) { + Log.d(TAG, "openDoor: $cellName") + canService.ledGreen(cellName) // 先亮绿灯 + canService.openDoor(cellName) // 再开锁 + } + + /** + * 批量开门 + */ + fun openDoors(cellNames: List) { + Log.d(TAG, "openDoors: $cellNames") + _openCellList.value = cellNames + cellNames.forEach { cellName -> + canService.ledGreen(cellName) + } + cellNames.forEach { cellName -> + canService.openDoor(cellName) + } + } + + /** + * 查询门状态 + */ + fun queryDoorStatus(cellName: String) { + canService.queryDoorStatus(cellName) + } + + // ==================== 电源控制 ==================== + + /** + * 格口上电 + */ + fun powerOn(cellName: String) { + canService.powerOn(cellName) + } + + /** + * 格口断电 + */ + fun powerOff(cellName: String) { + canService.powerOff(cellName) + } + + /** + * 批量上电 + */ + fun powerOnAll(cellNames: List) { + canService.powerOnAll(cellNames) + } + + /** + * 批量断电 + */ + fun powerOffAll(cellNames: List) { + canService.powerOffAll(cellNames) + } + + // ==================== LED 控制 ==================== + + fun ledRed(cellName: String) = canService.ledRed(cellName) + fun ledGreen(cellName: String) = canService.ledGreen(cellName) + fun ledOff(cellName: String) = canService.ledOff(cellName) + fun ledOffAll(cellNames: List) = canService.ledOffAll(cellNames) + + // ==================== 设备信息 ==================== + + /** + * 读取设备序列号 + */ + fun readDeviceSn(cellName: String) { + canService.readDeviceSn(cellName) + } + + /** + * 读取设备连接状态 + */ + fun readConnectStatus(cellName: String) { + canService.readConnectStatus(cellName) + } + + // ==================== 业务操作 ==================== + + /** + * 借设备 + */ + fun borrowDevice(cellName: String, operator: String, userId: String) { + Log.d(TAG, "borrowDevice: $cellName by $operator") + _currentOperation.value = OperationType.OPT_BORROW + + // 亮绿灯 + canService.ledGreen(cellName) + // 开门 + canService.openDoor(cellName) + + // 上报 MQTT(如果连接) + if (mqttService.isMqttConnect) { + val event = DeviceEvent( + opType = "borrow", + deviceCode = _deviceSn.value, + cabinetName = cellName.take(1), + devSn = "", + doorName = cellName, + devType = "", + userId = userId, + batchId = generateBatchId(), + opTime = getCurrentTime(), + doorInfo = DoorInfo(cellName = cellName) + ) + mqttService.publishBorrowdevEvent(event) + } + } + + /** + * 还设备 + */ + fun returnDevice(cellName: String, operator: String, userId: String) { + Log.d(TAG, "returnDevice: $cellName by $operator") + _currentOperation.value = OperationType.OPT_RETURN + + // 亮绿灯 + canService.ledGreen(cellName) + // 开门 + canService.openDoor(cellName) + } + + /** + * 报修归还 + */ + fun repairReturn(cellName: String, operator: String) { + Log.d(TAG, "repairReturn: $cellName") + _currentOperation.value = OperationType.OPT_REPAIRDEVIN + + // 亮红灯 + canService.ledRed(cellName) + // 开门 + canService.openDoor(cellName) + } + + /** + * 结束当前操作 + */ + fun endOperation() { + Log.d(TAG, "endOperation") + _currentOperation.value = -1 + + // 关闭所有灯 + _openCellList.value.forEach { cellName -> + canService.ledOff(cellName) + } + _openCellList.value = emptyList() + } + + // ==================== 回调处理 ==================== + + private fun handleCanResponse(response: CanResponse) { + when (response.command) { + CanCommands.LON -> handleDoorOpenResponse(response) + CanCommands.LST -> handleDoorStatusResponse(response) + CanCommands.DSN -> handleDeviceSnResponse(response) + CanCommands.CON -> handleConnectStatusResponse(response) + CanCommands.PVD -> handlePvdResponse(response) + CanCommands.MFG -> handleMfgResponse(response) + CanCommands.PRO -> handleProResponse(response) + } + } + + private fun handleDoorOpenResponse(response: CanResponse) { + if (response.value == CanConstants.DOOR_OPEN_STATUS) { + Log.d(TAG, "${response.cellName} 开门成功") + // 开始监测门状态 + canService.queryDoorStatus(response.cellName) + } else { + Log.d(TAG, "${response.cellName} 开门失败") + } + } + + private fun handleDoorStatusResponse(response: CanResponse) { + if (response.value == CanConstants.DOOR_CLOSE_STATUS) { + Log.d(TAG, "${response.cellName} 门已关闭") + // 门关闭后的处理 + canService.ledOff(response.cellName) + + // 如果是还设备操作,读取设备SN + if (_currentOperation.value == OperationType.OPT_RETURN || + _currentOperation.value == OperationType.OPT_BATCHIN) { + canService.readDeviceSn(response.cellName) + } + } else { + // 门还开着,继续监测 + canService.queryDoorStatus(response.cellName) + } + } + + private fun handleDeviceSnResponse(response: CanResponse) { + Log.d(TAG, "${response.cellName} 设备SN: ${response.value}") + // 上报设备信息到 MQTT + if (mqttService.isMqttConnect) { + val doorInfo = DoorInfo( + cellName = response.cellName, + devSn = response.value + ) + mqttService.publishDb(doorInfo) + } + } + + private fun handleConnectStatusResponse(response: CanResponse) { + val connected = response.value == "1" + Log.d(TAG, "${response.cellName} 设备连接状态: $connected") + } + + private fun handlePvdResponse(response: CanResponse) { + Log.d(TAG, "${response.cellName} PVD: ${response.value}") + } + + private fun handleMfgResponse(response: CanResponse) { + Log.d(TAG, "${response.cellName} MFG: ${response.value}") + } + + private fun handleProResponse(response: CanResponse) { + Log.d(TAG, "${response.cellName} PRO: ${response.value}") + } + + private fun handleMqttMessage(topic: String, message: String) { + // 处理 MQTT 下发的命令 + // 根据 topic 和 message 执行相应操作 + } + + // ==================== 工具方法 ==================== + + private fun generateBatchId(): String { + return (1..15).map { (0..9).random() }.joinToString("") + } + + private fun getCurrentTime(): String { + val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()) + return sdf.format(java.util.Date()) + } +} + +// ==================== Compose 集成 ==================== + +@Composable +fun rememberDeviceManager(): DeviceManager { + val context = LocalContext.current + return remember { DeviceManager.getInstance(context) } +} + +@Composable +fun rememberMqttConnectionState(): MqttConnectionState { + val manager = rememberDeviceManager() + val state by manager.mqttConnectionState.collectAsState() + return state +} + +@Composable +fun rememberCanConnectionState(): Boolean { + val manager = rememberDeviceManager() + val state by manager.canConnectionState.collectAsState() + return state +} diff --git a/app/src/main/java/com/example/smarthome/data/LanguageManager.kt b/app/src/main/java/com/example/smarthome/data/LanguageManager.kt new file mode 100644 index 0000000..e0cae88 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/data/LanguageManager.kt @@ -0,0 +1,437 @@ +package com.example.smarthome.data + +import android.app.Activity +import android.content.Context +import android.content.Intent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * 语言管理器 - 管理应用多语言 + */ +object LanguageManager { + + // 支持的语言 + enum class Language(val code: String, val displayName: String, val nativeName: String) { + CHINESE("zh", "中文", "简体中文"), + ENGLISH("en", "English", "English"), + KOREAN("ko", "한국어", "한국어"), + JAPANESE("ja", "日本語", "日本語"), + RUSSIAN("ru", "Русский", "Русский") + } + + private val _currentLanguage = MutableStateFlow(Language.CHINESE) + val currentLanguage: StateFlow = _currentLanguage + + private const val PREFS_NAME = "language_prefs" + private const val KEY_LANGUAGE = "selected_language" + + fun init(context: Context) { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val savedCode = prefs.getString(KEY_LANGUAGE, Language.CHINESE.code) ?: Language.CHINESE.code + _currentLanguage.value = Language.entries.find { it.code == savedCode } ?: Language.CHINESE + } + + fun setLanguage(context: Context, language: Language) { + val currentLang = _currentLanguage.value + if (currentLang == language) return // 语言没变,不需要重启 + + // 先同步保存语言设置(使用commit确保保存完成后再重启) + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putString(KEY_LANGUAGE, language.code) + .commit() // 使用commit同步保存,而不是apply异步保存 + + _currentLanguage.value = language + + // 重启应用 + restartApp(context) + } + + private fun restartApp(context: Context) { + val packageManager = context.packageManager + val intent = packageManager.getLaunchIntentForPackage(context.packageName) + intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + context.startActivity(intent) + if (context is Activity) { + context.finish() + } + Runtime.getRuntime().exit(0) + } + + // 获取翻译文本 + fun getString(key: String): String { + return translations[_currentLanguage.value]?.get(key) ?: translations[Language.CHINESE]?.get(key) ?: key + } + + // 翻译字典 + private val translations: Map> = mapOf( + Language.CHINESE to mapOf( + // 导航 + "nav_dashboard" to "控制台", + "nav_scene" to "场景", + "nav_automation" to "自动化", + "nav_statistics" to "统计", + "nav_security" to "安全", + "nav_settings" to "设置", + + // 设置页面 + "settings_title" to "设置", + "settings_account" to "账户设置", + "settings_profile" to "个人信息", + "settings_profile_desc" to "管理您的个人资料", + "settings_security" to "账户安全", + "settings_security_desc" to "密码和安全设置", + "settings_device" to "设备管理", + "settings_auto_connect" to "自动连接", + "settings_auto_connect_desc" to "自动连接到家庭网络", + "settings_notifications" to "设备通知", + "settings_notifications_desc" to "接收设备状态通知", + "settings_display" to "显示设置", + "settings_background" to "背景壁纸", + "settings_dark_mode" to "深色模式", + "settings_dark_mode_desc" to "使用深色主题", + "settings_language" to "语言", + "settings_about" to "关于", + "settings_version" to "版本信息", + "settings_privacy" to "隐私政策", + "settings_privacy_desc" to "查看隐私政策", + "settings_terms" to "用户协议", + "settings_terms_desc" to "查看用户协议", + "settings_logout" to "退出登录", + + // 控制台 + "dashboard_title" to "控制台", + "room_all" to "全屋", + "room_living" to "客厅", + "room_bedroom" to "卧室", + "room_kitchen" to "厨房", + "room_bathroom" to "浴室", + + // 空调 + "ac_title" to "空调", + "ac_mode_cool" to "制冷", + "ac_mode_heat" to "制热", + "ac_mode_dry" to "除湿", + "ac_mode_fan" to "送风", + "ac_fan_speed" to "风速", + "ac_fan_auto" to "自动", + "ac_fan_low" to "低", + "ac_fan_medium" to "中", + "ac_fan_high" to "高", + + // 灯光 + "light_title" to "灯光", + "light_brightness" to "亮度", + + // 窗帘 + "curtain_title" to "窗帘", + + // 天气 + "weather_humidity" to "湿度", + + // 通用 + "add_device" to "+ 添加设备", + "on" to "开", + "off" to "关", + "cancel" to "取消", + "confirm" to "确认", + "select_language" to "选择语言" + ), + + Language.ENGLISH to mapOf( + // Navigation + "nav_dashboard" to "Dashboard", + "nav_scene" to "Scenes", + "nav_automation" to "Automation", + "nav_statistics" to "Statistics", + "nav_security" to "Security", + "nav_settings" to "Settings", + + // Settings + "settings_title" to "Settings", + "settings_account" to "Account Settings", + "settings_profile" to "Profile", + "settings_profile_desc" to "Manage your profile", + "settings_security" to "Account Security", + "settings_security_desc" to "Password and security settings", + "settings_device" to "Device Management", + "settings_auto_connect" to "Auto Connect", + "settings_auto_connect_desc" to "Auto connect to home network", + "settings_notifications" to "Device Notifications", + "settings_notifications_desc" to "Receive device status notifications", + "settings_display" to "Display Settings", + "settings_background" to "Background", + "settings_dark_mode" to "Dark Mode", + "settings_dark_mode_desc" to "Use dark theme", + "settings_language" to "Language", + "settings_about" to "About", + "settings_version" to "Version", + "settings_privacy" to "Privacy Policy", + "settings_privacy_desc" to "View privacy policy", + "settings_terms" to "Terms of Service", + "settings_terms_desc" to "View terms of service", + "settings_logout" to "Log Out", + + // Dashboard + "dashboard_title" to "Dashboard", + "room_all" to "All Rooms", + "room_living" to "Living Room", + "room_bedroom" to "Bedroom", + "room_kitchen" to "Kitchen", + "room_bathroom" to "Bathroom", + + // AC + "ac_title" to "Air Conditioner", + "ac_mode_cool" to "Cool", + "ac_mode_heat" to "Heat", + "ac_mode_dry" to "Dry", + "ac_mode_fan" to "Fan", + "ac_fan_speed" to "Fan Speed", + "ac_fan_auto" to "Auto", + "ac_fan_low" to "Low", + "ac_fan_medium" to "Med", + "ac_fan_high" to "High", + + // Light + "light_title" to "Light", + "light_brightness" to "Brightness", + + // Curtain + "curtain_title" to "Curtain", + + // Weather + "weather_humidity" to "Humidity", + + // Common + "add_device" to "+ Add Device", + "on" to "On", + "off" to "Off", + "cancel" to "Cancel", + "confirm" to "Confirm", + "select_language" to "Select Language" + ), + + Language.KOREAN to mapOf( + // 네비게이션 + "nav_dashboard" to "대시보드", + "nav_scene" to "장면", + "nav_automation" to "자동화", + "nav_statistics" to "통계", + "nav_security" to "보안", + "nav_settings" to "설정", + + // 설정 + "settings_title" to "설정", + "settings_account" to "계정 설정", + "settings_profile" to "프로필", + "settings_profile_desc" to "프로필 관리", + "settings_security" to "계정 보안", + "settings_security_desc" to "비밀번호 및 보안 설정", + "settings_device" to "기기 관리", + "settings_auto_connect" to "자동 연결", + "settings_auto_connect_desc" to "홈 네트워크에 자동 연결", + "settings_notifications" to "기기 알림", + "settings_notifications_desc" to "기기 상태 알림 받기", + "settings_display" to "디스플레이 설정", + "settings_background" to "배경화면", + "settings_dark_mode" to "다크 모드", + "settings_dark_mode_desc" to "다크 테마 사용", + "settings_language" to "언어", + "settings_about" to "정보", + "settings_version" to "버전 정보", + "settings_privacy" to "개인정보 처리방침", + "settings_privacy_desc" to "개인정보 처리방침 보기", + "settings_terms" to "이용약관", + "settings_terms_desc" to "이용약관 보기", + "settings_logout" to "로그아웃", + + // 대시보드 + "dashboard_title" to "대시보드", + "room_all" to "전체", + "room_living" to "거실", + "room_bedroom" to "침실", + "room_kitchen" to "주방", + "room_bathroom" to "욕실", + + // 에어컨 + "ac_title" to "에어컨", + "ac_mode_cool" to "냉방", + "ac_mode_heat" to "난방", + "ac_mode_dry" to "제습", + "ac_mode_fan" to "송풍", + "ac_fan_speed" to "풍속", + "ac_fan_auto" to "자동", + "ac_fan_low" to "약", + "ac_fan_medium" to "중", + "ac_fan_high" to "강", + + // 조명 + "light_title" to "조명", + "light_brightness" to "밝기", + + // 커튼 + "curtain_title" to "커튼", + + // 날씨 + "weather_humidity" to "습도", + + // 공통 + "add_device" to "+ 기기 추가", + "on" to "켜짐", + "off" to "꺼짐", + "cancel" to "취소", + "confirm" to "확인", + "select_language" to "언어 선택" + ), + + Language.JAPANESE to mapOf( + // ナビゲーション + "nav_dashboard" to "ダッシュボード", + "nav_scene" to "シーン", + "nav_automation" to "オートメーション", + "nav_statistics" to "統計", + "nav_security" to "セキュリティ", + "nav_settings" to "設定", + + // 設定 + "settings_title" to "設定", + "settings_account" to "アカウント設定", + "settings_profile" to "プロフィール", + "settings_profile_desc" to "プロフィールを管理", + "settings_security" to "アカウントセキュリティ", + "settings_security_desc" to "パスワードとセキュリティ設定", + "settings_device" to "デバイス管理", + "settings_auto_connect" to "自動接続", + "settings_auto_connect_desc" to "ホームネットワークに自動接続", + "settings_notifications" to "デバイス通知", + "settings_notifications_desc" to "デバイス状態の通知を受信", + "settings_display" to "表示設定", + "settings_background" to "背景", + "settings_dark_mode" to "ダークモード", + "settings_dark_mode_desc" to "ダークテーマを使用", + "settings_language" to "言語", + "settings_about" to "情報", + "settings_version" to "バージョン情報", + "settings_privacy" to "プライバシーポリシー", + "settings_privacy_desc" to "プライバシーポリシーを見る", + "settings_terms" to "利用規約", + "settings_terms_desc" to "利用規約を見る", + "settings_logout" to "ログアウト", + + // ダッシュボード + "dashboard_title" to "ダッシュボード", + "room_all" to "全室", + "room_living" to "リビング", + "room_bedroom" to "寝室", + "room_kitchen" to "キッチン", + "room_bathroom" to "バスルーム", + + // エアコン + "ac_title" to "エアコン", + "ac_mode_cool" to "冷房", + "ac_mode_heat" to "暖房", + "ac_mode_dry" to "除湿", + "ac_mode_fan" to "送風", + "ac_fan_speed" to "風速", + "ac_fan_auto" to "自動", + "ac_fan_low" to "弱", + "ac_fan_medium" to "中", + "ac_fan_high" to "強", + + // 照明 + "light_title" to "照明", + "light_brightness" to "明るさ", + + // カーテン + "curtain_title" to "カーテン", + + // 天気 + "weather_humidity" to "湿度", + + // 共通 + "add_device" to "+ デバイス追加", + "on" to "オン", + "off" to "オフ", + "cancel" to "キャンセル", + "confirm" to "確認", + "select_language" to "言語を選択" + ), + + Language.RUSSIAN to mapOf( + // Навигация + "nav_dashboard" to "Панель", + "nav_scene" to "Сцены", + "nav_automation" to "Автоматизация", + "nav_statistics" to "Статистика", + "nav_security" to "Безопасность", + "nav_settings" to "Настройки", + + // Настройки + "settings_title" to "Настройки", + "settings_account" to "Настройки аккаунта", + "settings_profile" to "Профиль", + "settings_profile_desc" to "Управление профилем", + "settings_security" to "Безопасность аккаунта", + "settings_security_desc" to "Пароль и настройки безопасности", + "settings_device" to "Управление устройствами", + "settings_auto_connect" to "Автоподключение", + "settings_auto_connect_desc" to "Автоподключение к домашней сети", + "settings_notifications" to "Уведомления", + "settings_notifications_desc" to "Получать уведомления о состоянии", + "settings_display" to "Настройки дисплея", + "settings_background" to "Фон", + "settings_dark_mode" to "Тёмный режим", + "settings_dark_mode_desc" to "Использовать тёмную тему", + "settings_language" to "Язык", + "settings_about" to "О приложении", + "settings_version" to "Версия", + "settings_privacy" to "Политика конфиденциальности", + "settings_privacy_desc" to "Просмотреть политику", + "settings_terms" to "Условия использования", + "settings_terms_desc" to "Просмотреть условия", + "settings_logout" to "Выйти", + + // Панель + "dashboard_title" to "Панель", + "room_all" to "Все комнаты", + "room_living" to "Гостиная", + "room_bedroom" to "Спальня", + "room_kitchen" to "Кухня", + "room_bathroom" to "Ванная", + + // Кондиционер + "ac_title" to "Кондиционер", + "ac_mode_cool" to "Охлаждение", + "ac_mode_heat" to "Обогрев", + "ac_mode_dry" to "Осушение", + "ac_mode_fan" to "Вентиляция", + "ac_fan_speed" to "Скорость", + "ac_fan_auto" to "Авто", + "ac_fan_low" to "Низ", + "ac_fan_medium" to "Сред", + "ac_fan_high" to "Выс", + + // Освещение + "light_title" to "Свет", + "light_brightness" to "Яркость", + + // Шторы + "curtain_title" to "Шторы", + + // Погода + "weather_humidity" to "Влажность", + + // Общее + "add_device" to "+ Добавить", + "on" to "Вкл", + "off" to "Выкл", + "cancel" to "Отмена", + "confirm" to "ОК", + "select_language" to "Выбрать язык" + ) + ) +} + +// 便捷函数 +fun tr(key: String): String = LanguageManager.getString(key) diff --git a/app/src/main/java/com/example/smarthome/data/MqttService.kt b/app/src/main/java/com/example/smarthome/data/MqttService.kt new file mode 100644 index 0000000..ad3fa5d --- /dev/null +++ b/app/src/main/java/com/example/smarthome/data/MqttService.kt @@ -0,0 +1,486 @@ +package com.example.smarthome.data + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.json.JSONObject +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * MQTT 服务 + * + * 依赖库(需要在 build.gradle 中添加): + * implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5") + * implementation("org.eclipse.paho:org.eclipse.paho.android.service:1.1.1") + * + * AndroidManifest.xml 需要添加: + * + */ + +// ==================== 常量定义(与原代码一致)==================== + +object MqttConstants { + const val TAG = "MqttService" + + // 默认服务器配置 + const val HOST_DOMAIN = "mqtt.cd-iot.cn" + const val HOST_DOMAIN_API = "https://www.cd-iot.cn" + const val HOST_PORT = ":1884" + + // 主题前缀 + const val PUBLISH_HEAD = "bcup/0jnqd7slms9vvaf8/" + const val PUBLISH_TAIL_EVENT = "/event" + const val PUBLISH_TAIL_PROP = "/prop" + const val PUBLISH_TAIL_UPDATEREPLY = "/upgrade/reply" + const val PUBLISH_TAIL_MESSAGEREPLY = "/message/reply" + + const val SUBSCRIBE_HEAD = "bcdown/0jnqd7slms9vvaf8/" + const val SUBSCRIBE_TAIL_UPDATE = "/upgrade/set" + const val SUBSCRIBE_TAIL_PROPERTYSET = "/property/set" + const val SUBSCRIBE_TAIL_SERVER = "/service/invoke" + + // QoS 等级 + const val QOS_DEFAULT = 2 +} + +// ==================== 数据模型 ==================== + +/** + * MQTT 连接状态 + */ +enum class MqttConnectionState { + DISCONNECTED, + CONNECTING, + CONNECTED, + CONNECTION_LOST +} + +/** + * 设备信息(与原代码 Door 实体对应) + */ +data class DoorInfo( + val cellName: String = "", // 格口名称 + val cabinetName: String = "", // 柜子名称 + val cellState: String = "", // 格口状态 + val cellVoltageType: String = "", // 电压类型 + val cellConnectorType: String = "", // 接口类型 + val cellRepairType: String = "", // 格口维修类型 + val devType: String = "", // 设备类型 + val devCheckType: String = "", // 设备检测类型 + val devSn: String = "", // 设备序列号 + val devPvd: String = "", // 设备PID/VID + val devPro: String = "", // 设备产品名 + val devMfg: String = "", // 设备制造商 + val devPwr: String = "", // 设备电量 + val devState: String = "", // 设备状态 + val devRepairType: String = "" // 设备维修类型 +) + +/** + * 借还设备事件 + */ +data class DeviceEvent( + val opType: String, // 操作类型 + val deviceCode: String, // 设备编码(柜子SN) + val cabinetName: String, // 柜子名称 + val devSn: String, // 设备序列号 + val doorName: String, // 格口名称 + val devType: String, // 设备类型 + val userId: String, // 用户ID + val batchId: String, // 批次ID + val opTime: String, // 操作时间 + val doorInfo: DoorInfo // 格口信息 +) + +// ==================== MQTT 服务接口 ==================== + +interface IMqttService { + val connectionState: StateFlow + val isMqttConnect: Boolean + + // 初始化和连接 + fun mqttInit(hostIp: String, deviceSn: String, devicePassword: String) + fun mqttConnect() + fun mqttClose() + + // 订阅 + fun subscribeUpdate() + fun subscribeLog() + fun subscribeUploadLog() + + // 发布 - 设备状态同步 + fun publishDb(doorInfo: DoorInfo) + fun publishBorrowdevEvent(event: DeviceEvent) + fun publishReturndevEvent(event: DeviceEvent) + fun publishLosedevEvent(event: DeviceEvent) + + // 发布 - 系统消息 + fun publishSoftwareVersion(version: String) + fun publishDownLoadReply(code: String, messageId: String, taskId: String) + fun publishLogupload(messageId: String, filename: String, fileUrl: String) + fun publishInitDeviceReply(messageId: String, opType: String) + fun publishCellSetReply(messageId: String) + + // 回调设置 + fun setMessageCallback(callback: (topic: String, message: String) -> Unit) + fun setConnectionCallback(callback: (MqttConnectionState) -> Unit) +} + +// ==================== MQTT 服务实现(模拟)==================== + +class MqttServiceImpl private constructor( + private val context: Context +) : IMqttService { + + companion object { + @Volatile + private var instance: MqttServiceImpl? = null + + fun getInstance(context: Context): MqttServiceImpl { + return instance ?: synchronized(this) { + instance ?: MqttServiceImpl(context.applicationContext).also { instance = it } + } + } + } + + // 状态 + private val _connectionState = MutableStateFlow(MqttConnectionState.DISCONNECTED) + override val connectionState: StateFlow = _connectionState.asStateFlow() + override val isMqttConnect: Boolean get() = _connectionState.value == MqttConnectionState.CONNECTED + + // 配置 + private var hostIp: String = "" + private var deviceSn: String = "" + private var clientId: String = "" + private var username: String = "" + private var password: String = "" + + // 主题 + private var topicPublishDb: String = "" + private var topicPublishSoftwareVersion: String = "" + private var topicPublishUpdateReply: String = "" + private var topicPublishLogUpload: String = "" + private var topicPublishInitDeviceReply: String = "" + private var topicPublishBorrowDev: String = "" + private var topicPublishReturnDev: String = "" + private var topicPublishLoseDev: String = "" + private var topicSubscribeUpdate: String = "" + private var topicSubscribePropertySet: String = "" + private var topicSubscribeServer: String = "" + + // 回调 + private var messageCallback: ((String, String) -> Unit)? = null + private var connectionCallback: ((MqttConnectionState) -> Unit)? = null + + // SharedPreferences + private val sharedPreferences: SharedPreferences by lazy { + context.getSharedPreferences("mqtt_prefs", Context.MODE_PRIVATE) + } + + // TODO: 真实 MQTT 客户端 + // private var mqttAndroidClient: MqttAndroidClient? = null + // private var mqttConnectOptions: MqttConnectOptions? = null + + // ==================== 初始化 ==================== + + override fun mqttInit(hostIp: String, deviceSn: String, devicePassword: String) { + if (hostIp.isEmpty()) { + Log.w(MqttConstants.TAG, "mqttInit: hostIp is empty") + return + } + + Log.d(MqttConstants.TAG, "mqttInit") + this.hostIp = hostIp + this.deviceSn = deviceSn + + // 初始化主题(与原代码一致) + topicPublishDb = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}" + topicPublishSoftwareVersion = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_PROP}" + topicPublishUpdateReply = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_UPDATEREPLY}" + topicPublishLogUpload = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_MESSAGEREPLY}" + topicPublishInitDeviceReply = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_MESSAGEREPLY}" + topicPublishBorrowDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}" + topicPublishReturnDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}" + topicPublishLoseDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}" + topicSubscribeUpdate = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_UPDATE}" + topicSubscribePropertySet = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_PROPERTYSET}" + topicSubscribeServer = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_SERVER}" + + // 生成认证信息(与原代码一致) + clientId = deviceSn + username = "$deviceSn|${System.currentTimeMillis()}" + password = md5("$username|$devicePassword") + + Log.d(MqttConstants.TAG, "CLIENTID: $clientId") + Log.d(MqttConstants.TAG, "USERNAME: $username") + + val serverUri = "tcp://$hostIp${MqttConstants.HOST_PORT}" + Log.d(MqttConstants.TAG, "serverURI: $serverUri") + + // TODO: 初始化真实 MQTT 客户端 + // mqttAndroidClient = MqttAndroidClient(context, serverUri, clientId) + // mqttAndroidClient?.setCallback(mqttCallback) + // mqttConnectOptions = MqttConnectOptions().apply { + // isCleanSession = true + // connectionTimeout = 10 + // keepAliveInterval = 20 + // userName = this@MqttServiceImpl.username + // password = this@MqttServiceImpl.password.toCharArray() + // } + } + + override fun mqttConnect() { + Log.d(MqttConstants.TAG, "mqttConnect") + _connectionState.value = MqttConnectionState.CONNECTING + connectionCallback?.invoke(MqttConnectionState.CONNECTING) + + // TODO: 真实连接 + // try { + // mqttAndroidClient?.connect(mqttConnectOptions, null, object : IMqttActionListener { + // override fun onSuccess(asyncActionToken: IMqttToken?) { + // Log.d(TAG, "MQTT连接成功!") + // _connectionState.value = MqttConnectionState.CONNECTED + // connectionCallback?.invoke(MqttConnectionState.CONNECTED) + // publishSoftwareVersion(getSoftwareVersion()) + // subscribeUpdate() + // subscribeLog() + // subscribeUploadLog() + // } + // override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { + // Log.d(TAG, "MQTT连接失败!") + // mqttClose() + // } + // }) + // } catch (e: MqttException) { + // e.printStackTrace() + // } + + // 模拟连接成功 + _connectionState.value = MqttConnectionState.CONNECTED + connectionCallback?.invoke(MqttConnectionState.CONNECTED) + sharedPreferences.edit().putBoolean("connected", true).apply() + } + + override fun mqttClose() { + Log.d(MqttConstants.TAG, "mqttClose") + _connectionState.value = MqttConnectionState.DISCONNECTED + connectionCallback?.invoke(MqttConnectionState.DISCONNECTED) + sharedPreferences.edit().putBoolean("connected", false).apply() + + // TODO: 断开真实连接 + // try { + // mqttAndroidClient?.disconnect() + // } catch (e: MqttException) { + // e.printStackTrace() + // } + } + + // ==================== 订阅 ==================== + + override fun subscribeUpdate() { + if (!isMqttConnect) return + Log.d(MqttConstants.TAG, "subscribeUpdate: $topicSubscribeUpdate") + // TODO: mqttAndroidClient?.subscribe(topicSubscribeUpdate, MqttConstants.QOS_DEFAULT) + } + + override fun subscribeLog() { + if (!isMqttConnect) return + Log.d(MqttConstants.TAG, "subscribeLog: $topicSubscribePropertySet") + // TODO: mqttAndroidClient?.subscribe(topicSubscribePropertySet, MqttConstants.QOS_DEFAULT) + } + + override fun subscribeUploadLog() { + if (!isMqttConnect) return + Log.d(MqttConstants.TAG, "subscribeUploadLog: $topicSubscribeServer") + // TODO: mqttAndroidClient?.subscribe(topicSubscribeServer, MqttConstants.QOS_DEFAULT) + } + + // ==================== 发布 - 设备状态 ==================== + + override fun publishDb(doorInfo: DoorInfo) { + val message = buildDoorInfoSyncEvent(doorInfo) + publish(topicPublishDb, message) + } + + override fun publishBorrowdevEvent(event: DeviceEvent) { + val message = buildBorrowDeviceEvent(event) + publish(topicPublishBorrowDev, message) + } + + override fun publishReturndevEvent(event: DeviceEvent) { + val message = buildReturnDeviceEvent(event) + publish(topicPublishReturnDev, message) + } + + override fun publishLosedevEvent(event: DeviceEvent) { + val message = buildLoseDeviceEvent(event) + publish(topicPublishLoseDev, message) + } + + // ==================== 发布 - 系统消息 ==================== + + override fun publishSoftwareVersion(version: String) { + val message = """{"identifier":"firmware_version","firmware_version":"$version"}""" + publish(topicPublishSoftwareVersion, message) + } + + override fun publishDownLoadReply(code: String, messageId: String, taskId: String) { + val replyMsg = when (code) { + "0" -> "等待升级" + "1" -> "已发送设备" + "2" -> "设备收到" + "3" -> "升级成功" + "4" -> "升级失败" + "5" -> "设备离线停止推送" + else -> "未知错误码" + } + val message = """{"messageId":"$messageId","result":{"code":"$code","msg":"$replyMsg","taskId":"$taskId"}}""" + publish(topicPublishUpdateReply, message) + } + + override fun publishLogupload(messageId: String, filename: String, fileUrl: String) { + val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"upload_log_reply","fileName":"$filename","fileUrl":"$fileUrl"}}}""" + publish(topicPublishLogUpload, message) + } + + override fun publishInitDeviceReply(messageId: String, opType: String) { + val dstr = getDataString() + val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"init_device_reply","status":"ok","optype":"$opType","time":"$dstr"}}}""" + publish(topicPublishInitDeviceReply, message) + } + + override fun publishCellSetReply(messageId: String) { + val dstr = getDataString() + val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"door_set_reply","status":"ok","time":"$dstr"}}}""" + publish(topicPublishInitDeviceReply, message) + } + + // ==================== 回调 ==================== + + override fun setMessageCallback(callback: (topic: String, message: String) -> Unit) { + messageCallback = callback + } + + override fun setConnectionCallback(callback: (MqttConnectionState) -> Unit) { + connectionCallback = callback + } + + // ==================== 内部方法 ==================== + + private fun publish(topic: String, message: String) { + if (!isMqttConnect) { + Log.w(MqttConstants.TAG, "publish failed: not connected") + return + } + Log.d(MqttConstants.TAG, "publish to $topic: $message") + + // TODO: 真实发布 + // try { + // mqttAndroidClient?.publish(topic, message.toByteArray(), MqttConstants.QOS_DEFAULT, false) + // } catch (e: MqttException) { + // e.printStackTrace() + // } + } + + /** + * 处理收到的消息(由 MqttCallback 调用) + */ + internal fun handleMessage(topic: String, payload: String) { + Log.d(MqttConstants.TAG, "MQTT收到消息:topic:$topic msg:$payload") + messageCallback?.invoke(topic, payload) + + try { + when (topic) { + topicSubscribeUpdate -> handleUpdateMessage(payload) + topicSubscribePropertySet -> handlePropertySetMessage(payload) + topicSubscribeServer -> handleServerMessage(payload) + } + } catch (e: Exception) { + Log.e(MqttConstants.TAG, "handleMessage error", e) + } + } + + private fun handleUpdateMessage(payload: String) { + val jsonObject = JSONObject(payload) + val body = jsonObject.getString("body") + val bodyJson = JSONObject(body) + + val messageId = bodyJson.getString("messageId") + val taskId = bodyJson.getString("taskId") + val firmwareVersion = bodyJson.getString("firmwareVersion") + val url = bodyJson.getString("url") + + sharedPreferences.edit().apply { + putString("fw", firmwareVersion) + putString("url", url) + putString("mid", messageId) + putString("tid", taskId) + putString("vcheck", "1") + apply() + } + } + + private fun handlePropertySetMessage(payload: String) { + val jsonObject = JSONObject(payload) + val identifier = jsonObject.getString("identifier") + val body = jsonObject.getString("body") + Log.d(MqttConstants.TAG, "PropertySet: $identifier = $body") + } + + private fun handleServerMessage(payload: String) { + val jsonObject = JSONObject(payload) + val identifier = jsonObject.getString("identifier") + Log.d(MqttConstants.TAG, "Server identifier: $identifier") + // 根据 identifier 处理不同的服务调用 + } + + // ==================== 消息构建 ==================== + + private fun buildDoorInfoSyncEvent(info: DoorInfo): String { + return """{"door_info_sync_event":{"devType":"${info.devType}","devSn":"${info.devSn}","devCheckType":"${info.devCheckType}","cabinetName":"${info.cabinetName}","doorState":"${info.cellState}","connectorType":"${info.cellConnectorType}","devPvd":"${info.devPvd}","doorName":"${info.cellName}","doorRepairType":"${info.cellRepairType}","devRepairType":"${info.devRepairType}","devPro":"${info.devPro}","devState":"${info.devState}","devMfg":"${info.devMfg}","devPwr":"${info.devPwr}","doorVoltageType":"${info.cellVoltageType}"}}""" + } + + private fun buildBorrowDeviceEvent(event: DeviceEvent): String { + val doorInfo = event.doorInfo + return """{"borrow_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","borrowTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}""" + } + + private fun buildReturnDeviceEvent(event: DeviceEvent): String { + val doorInfo = event.doorInfo + return """{"return_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","doorRepairType":"${doorInfo.cellRepairType}","devRepairType":"${doorInfo.devRepairType}","returnTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}""" + } + + private fun buildLoseDeviceEvent(event: DeviceEvent): String { + val doorInfo = event.doorInfo + return """{"lose_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","doorRepairType":"${doorInfo.cellRepairType}","devRepairType":"${doorInfo.devRepairType}","loseTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}""" + } + + private fun buildDoorInfoJson(info: DoorInfo): String { + return """{"devType":"${info.devType}","devSn":"${info.devSn}","devCheckType":"${info.devCheckType}","cabinetName":"${info.cabinetName}","doorState":"${info.cellState}","connectorType":"${info.cellConnectorType}","devPvd":"${info.devPvd}","doorName":"${info.cellName}","doorRepairType":"${info.cellRepairType}","devRepairType":"${info.devRepairType}","devPro":"${info.devPro}","devState":"${info.devState}","devMfg":"${info.devMfg}","devPwr":"${info.devPwr}","doorVoltageType":"${info.cellVoltageType}"}""" + } + + // ==================== 工具方法 ==================== + + private fun md5(input: String): String { + return try { + val md = MessageDigest.getInstance("MD5") + val digest = md.digest(input.toByteArray()) + digest.joinToString("") { "%02x".format(it) } + } catch (e: NoSuchAlgorithmException) { + "" + } + } + + private fun getDataString(): String { + val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()) + return sdf.format(java.util.Date()) + } +} + +// ==================== 便捷扩展 ==================== + +fun Context.getMqttService(): IMqttService = MqttServiceImpl.getInstance(this) diff --git a/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt index b24cbf0..082927c 100644 --- a/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt +++ b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt @@ -32,6 +32,8 @@ import androidx.compose.foundation.border import com.example.smarthome.R import com.example.smarthome.data.WeatherInfo import com.example.smarthome.data.WeatherService +import com.example.smarthome.data.LanguageManager +import com.example.smarthome.data.tr import kotlinx.coroutines.launch import kotlinx.coroutines.delay import androidx.compose.ui.platform.LocalDensity @@ -139,10 +141,10 @@ fun RoomContent(selectedRoom: Int, roomName: String) { fun OverviewRoomContent() { Column(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { - AirConditionerCard(modifier = Modifier.weight(1f), roomName = "全屋") - UsageStatusChart(modifier = Modifier.weight(1f), roomName = "全屋") + AirConditionerCard(modifier = Modifier.weight(1f), roomName = tr("room_all")) + UsageStatusChart(modifier = Modifier.weight(1f), roomName = tr("room_all")) } - LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = "全屋") + LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = tr("room_all")) ModeButtonsRow(onModeSelected = { }) Spacer(modifier = Modifier.height(24.dp)) @@ -502,14 +504,17 @@ fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) { verticalArrangement = Arrangement.SpaceBetween ) { // 上半部分:导航项 + // 监听语言变化以触发重组 + val currentLang by LanguageManager.currentLanguage.collectAsState() + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Spacer(modifier = Modifier.height(48.dp)) - NavRailItem("控制台", R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) }) - NavRailItem("场景", R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) }) - NavRailItem("自动化", R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) }) - NavRailItem("统计", R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) }) - NavRailItem("安全", R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) }) - NavRailItem("设置", R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) }) + NavRailItem(tr("nav_dashboard"), R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) }) + NavRailItem(tr("nav_scene"), R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) }) + NavRailItem(tr("nav_automation"), R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) }) + NavRailItem(tr("nav_statistics"), R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) }) + NavRailItem(tr("nav_security"), R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) }) + NavRailItem(tr("nav_settings"), R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) }) } // 底部:模式切换按钮(暂时注释) @@ -578,6 +583,7 @@ fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Un horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier + .width(80.dp) // 固定宽度,防止文字长度不一致导致图标不对齐 .scale(scale) .clickable( indication = null, @@ -615,9 +621,10 @@ fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Un Text( text = text, color = if (selected) Color.White else Color(0xFFB0B0B0), - fontSize = 13.sp, + fontSize = 12.sp, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, - maxLines = 1 + maxLines = 1, + textAlign = TextAlign.Center ) } } @@ -638,7 +645,7 @@ fun TopBar() { } Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { - Text(text = "控制台", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White) + Text(text = tr("dashboard_title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White) WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading) } } @@ -1123,12 +1130,12 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 // 模式emoji图标 val modeIcons = listOf( - "❄️" to "制冷", - "☀️" to "制热", - "💧" to "除湿", - "🌀" to "送风" + "❄️" to tr("ac_mode_cool"), + "☀️" to tr("ac_mode_heat"), + "💧" to tr("ac_mode_dry"), + "🌀" to tr("ac_mode_fan") ) - val fanSpeeds = listOf("自动", "低", "中", "高") + val fanSpeeds = listOf(tr("ac_fan_auto"), tr("ac_fan_low"), tr("ac_fan_medium"), tr("ac_fan_high")) // 图标动画 val infiniteTransition = rememberInfiniteTransition(label = "ac_icon") @@ -1212,7 +1219,7 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 ) } Column { - Text(text = "空调", fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp) + Text(text = tr("ac_title"), fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp) Text(text = roomName, fontSize = 11.sp, color = Color(0xFF9AA0A6)) } } @@ -1320,7 +1327,7 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically ) { - Text(text = "风速", fontSize = 12.sp, color = Color(0xFF9AA0A6)) + Text(text = tr("ac_fan_speed"), fontSize = 12.sp, color = Color(0xFF9AA0A6)) fanSpeeds.forEachIndexed { index, speed -> val isSelected = fanSpeed == index Box( diff --git a/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt b/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt index 103f99d..3146bd3 100644 --- a/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt +++ b/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt @@ -22,7 +22,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import com.example.smarthome.data.BackgroundManager +import com.example.smarthome.data.LanguageManager +import com.example.smarthome.data.tr @Composable fun SettingsScreen(onBack: () -> Unit) { @@ -44,52 +47,67 @@ fun SettingsScreen(onBack: () -> Unit) { @Composable fun SettingsContentList() { + val context = LocalContext.current + val currentLang by LanguageManager.currentLanguage.collectAsState() + var showLanguageDialog by remember { mutableStateOf(false) } + + // 语言选择对话框 + if (showLanguageDialog) { + LanguageSelectDialog( + currentLanguage = currentLang, + onLanguageSelect = { lang -> + LanguageManager.setLanguage(context, lang) + showLanguageDialog = false + }, + onDismiss = { showLanguageDialog = false } + ) + } + LazyColumn( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - item { SettingsSection(title = "账户设置") { + item { SettingsSection(title = tr("settings_account")) { SettingsItem( - title = "个人信息", - subtitle = "管理您的个人资料", + title = tr("settings_profile"), + subtitle = tr("settings_profile_desc"), onClick = { } ) SettingsItem( - title = "账户安全", - subtitle = "密码和安全设置", + title = tr("settings_security"), + subtitle = tr("settings_security_desc"), onClick = { } ) }} - item { SettingsSection(title = "设备管理") { + item { SettingsSection(title = tr("settings_device")) { var autoConnect by remember { mutableStateOf(true) } SettingsSwitchItem( - title = "自动连接", - subtitle = "自动连接到家庭网络", + title = tr("settings_auto_connect"), + subtitle = tr("settings_auto_connect_desc"), checked = autoConnect, onCheckedChange = { autoConnect = it } ) var notifications by remember { mutableStateOf(true) } SettingsSwitchItem( - title = "设备通知", - subtitle = "接收设备状态通知", + title = tr("settings_notifications"), + subtitle = tr("settings_notifications_desc"), checked = notifications, onCheckedChange = { notifications = it } ) }} - item { SettingsSection(title = "显示设置") { + item { SettingsSection(title = tr("settings_display")) { BackgroundSelector() - val context = LocalContext.current val selectedBg by BackgroundManager.selectedBackground.collectAsState() // 深色模式根据当前背景是否为暗色来判断 val isDarkMode = BackgroundManager.backgrounds.getOrNull(selectedBg)?.isDark ?: false SettingsSwitchItem( - title = "深色模式", - subtitle = "使用深色主题", + title = tr("settings_dark_mode"), + subtitle = tr("settings_dark_mode_desc"), checked = isDarkMode, onCheckedChange = { dark -> // 切换深色/浅色模式时自动更换背景 @@ -102,26 +120,26 @@ fun SettingsContentList() { ) SettingsItem( - title = "语言", - subtitle = "简体中文", - onClick = { } + title = tr("settings_language"), + subtitle = currentLang.nativeName, + onClick = { showLanguageDialog = true } ) }} - item { SettingsSection(title = "关于") { + item { SettingsSection(title = tr("settings_about")) { SettingsItem( - title = "版本信息", + title = tr("settings_version"), subtitle = "v1.0.0", onClick = { } ) SettingsItem( - title = "隐私政策", - subtitle = "查看隐私政策", + title = tr("settings_privacy"), + subtitle = tr("settings_privacy_desc"), onClick = { } ) SettingsItem( - title = "用户协议", - subtitle = "查看用户协议", + title = tr("settings_terms"), + subtitle = tr("settings_terms_desc"), onClick = { } ) }} @@ -133,6 +151,98 @@ fun SettingsContentList() { } } +@Composable +fun LanguageSelectDialog( + currentLanguage: LanguageManager.Language, + onLanguageSelect: (LanguageManager.Language) -> Unit, + onDismiss: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(Color(0xFF1E1E2E)) + .padding(20.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = tr("select_language"), + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + Spacer(modifier = Modifier.height(8.dp)) + + LanguageManager.Language.entries.forEach { lang -> + val isSelected = lang == currentLanguage + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background( + if (isSelected) Color(0xFF6C63FF).copy(alpha = 0.3f) + else Color(0xFF2A2A3E) + ) + .border( + width = if (isSelected) 2.dp else 0.dp, + color = if (isSelected) Color(0xFF6C63FF) else Color.Transparent, + shape = RoundedCornerShape(12.dp) + ) + .clickable { onLanguageSelect(lang) } + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = lang.nativeName, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = Color.White + ) + if (lang.displayName != lang.nativeName) { + Text( + text = lang.displayName, + fontSize = 12.sp, + color = Color(0xFF9AA0A6) + ) + } + } + if (isSelected) { + Text(text = "✓", color = Color(0xFF6C63FF), fontSize = 18.sp) + } + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 取消按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFF3A3A4E)) + .clickable { onDismiss() } + .padding(14.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = tr("cancel"), + color = Color.White, + fontSize = 14.sp + ) + } + } + } + } +} + @Composable fun SettingsTopBar(onBack: () -> Unit) { Row( @@ -158,7 +268,7 @@ fun SettingsTopBar(onBack: () -> Unit) { Text(text = "←", color = Color.White, fontSize = 20.sp) } Text( - text = "设置", + text = tr("settings_title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White @@ -344,7 +454,7 @@ fun LogoutButton(onClick: () -> Unit) { contentAlignment = Alignment.Center ) { Text( - text = "退出登录", + text = tr("settings_logout"), fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White diff --git a/app/src/main/res/drawable/background1.jpg b/app/src/main/res/drawable/background1.jpg new file mode 100644 index 0000000..e6adbd2 Binary files /dev/null and b/app/src/main/res/drawable/background1.jpg differ diff --git a/app/src/main/res/drawable/background1_thumb.jpg b/app/src/main/res/drawable/background1_thumb.jpg new file mode 100644 index 0000000..b5bb1b4 Binary files /dev/null and b/app/src/main/res/drawable/background1_thumb.jpg differ diff --git a/app/src/main/res/drawable/background2.jpg b/app/src/main/res/drawable/background2.jpg new file mode 100644 index 0000000..7c54f79 Binary files /dev/null and b/app/src/main/res/drawable/background2.jpg differ diff --git a/app/src/main/res/drawable/background2_thumb.jpg b/app/src/main/res/drawable/background2_thumb.jpg new file mode 100644 index 0000000..7b1ae98 Binary files /dev/null and b/app/src/main/res/drawable/background2_thumb.jpg differ diff --git a/app/src/main/res/drawable/background3.jpg b/app/src/main/res/drawable/background3.jpg new file mode 100644 index 0000000..f900ba4 Binary files /dev/null and b/app/src/main/res/drawable/background3.jpg differ diff --git a/app/src/main/res/drawable/background3_thumb.jpg b/app/src/main/res/drawable/background3_thumb.jpg new file mode 100644 index 0000000..2e0a3f5 Binary files /dev/null and b/app/src/main/res/drawable/background3_thumb.jpg differ diff --git a/app/src/main/res/drawable/ic_ac_cool.xml b/app/src/main/res/drawable/ic_ac_cool.xml new file mode 100644 index 0000000..76b87df --- /dev/null +++ b/app/src/main/res/drawable/ic_ac_cool.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ac_dry.xml b/app/src/main/res/drawable/ic_ac_dry.xml new file mode 100644 index 0000000..512dc9a --- /dev/null +++ b/app/src/main/res/drawable/ic_ac_dry.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ac_fan.xml b/app/src/main/res/drawable/ic_ac_fan.xml new file mode 100644 index 0000000..179030c --- /dev/null +++ b/app/src/main/res/drawable/ic_ac_fan.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ac_heat.xml b/app/src/main/res/drawable/ic_ac_heat.xml new file mode 100644 index 0000000..e8d6a4c --- /dev/null +++ b/app/src/main/res/drawable/ic_ac_heat.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/background2.jpg b/background2.jpg new file mode 100644 index 0000000..7c54f79 Binary files /dev/null and b/background2.jpg differ