feat(wear): add Wear OS app — Compose for Wear, timeline screen, timer chips, Material theme
This commit is contained in:
parent
9c34a92b9e
commit
ded0a0f0ea
66
android/wear/build.gradle.kts
Normal file
66
android/wear/build.gradle.kts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.chronomind.wear"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.chronomind.wear"
|
||||||
|
minSdk = 30
|
||||||
|
targetSdk = 35
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Core
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
|
||||||
|
// Wear Compose
|
||||||
|
implementation(libs.androidx.wear.compose.material)
|
||||||
|
implementation(libs.androidx.wear.compose.foundation)
|
||||||
|
|
||||||
|
// Compose core (needed for Wear Compose)
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
implementation(libs.androidx.compose.ui)
|
||||||
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
||||||
|
// Kotlin
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
|
// Tiles
|
||||||
|
implementation(libs.androidx.wear.tiles)
|
||||||
|
}
|
||||||
33
android/wear/src/main/AndroidManifest.xml
Normal file
33
android/wear/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="ChronoMind"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@android:style/Theme.DeviceDefault">
|
||||||
|
|
||||||
|
<uses-library
|
||||||
|
android:name="com.google.android.wearable"
|
||||||
|
android:required="true" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".WearMainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:taskAffinity="">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
package com.chronomind.wear
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
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.wear.compose.foundation.lazy.ScalingLazyColumn
|
||||||
|
import androidx.wear.compose.foundation.lazy.items
|
||||||
|
import androidx.wear.compose.material.*
|
||||||
|
|
||||||
|
class WearMainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
WearApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WearApp() {
|
||||||
|
MaterialTheme(
|
||||||
|
colors = Colors(
|
||||||
|
primary = Color(0xFF6C5CE7),
|
||||||
|
secondary = Color(0xFF00D2FF),
|
||||||
|
background = Color(0xFF0A0A0F),
|
||||||
|
surface = Color(0xFF14141F),
|
||||||
|
error = Color(0xFFFF5252),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
onBackground = Color(0xFFEEEEFF),
|
||||||
|
onSurface = Color(0xFFEEEEFF),
|
||||||
|
onError = Color.White,
|
||||||
|
onSurfaceVariant = Color(0xFFAAAACC),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
WearTimelineScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WearTimelineScreen() {
|
||||||
|
val timers = remember { mutableStateListOf<WearTimer>() }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
timeText = { TimeText() },
|
||||||
|
vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) }
|
||||||
|
) {
|
||||||
|
if (timers.isEmpty()) {
|
||||||
|
WearEmptyState()
|
||||||
|
} else {
|
||||||
|
ScalingLazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
items(timers) { timer ->
|
||||||
|
WearTimerChip(timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WearEmptyState() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = "⏱",
|
||||||
|
fontSize = 32.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "No timers",
|
||||||
|
color = Color(0xFFAAAACC),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Create from phone",
|
||||||
|
color = Color(0xFF666688),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WearTimerChip(timer: WearTimer) {
|
||||||
|
Chip(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = { /* navigate to detail */ },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = timer.label,
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
secondaryLabel = {
|
||||||
|
Text(
|
||||||
|
text = timer.formattedRemaining,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF6C5CE7)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Text(
|
||||||
|
text = when (timer.urgency) {
|
||||||
|
"critical" -> "🔴"
|
||||||
|
"important" -> "🟠"
|
||||||
|
"gentle" -> "🟢"
|
||||||
|
else -> "🔵"
|
||||||
|
},
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ChipDefaults.chipColors(
|
||||||
|
backgroundColor = Color(0xFF14141F)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Wear Timer Model (simplified from phone)
|
||||||
|
|
||||||
|
data class WearTimer(
|
||||||
|
val id: String,
|
||||||
|
val label: String,
|
||||||
|
val urgency: String,
|
||||||
|
val remainingSeconds: Double,
|
||||||
|
val state: String
|
||||||
|
) {
|
||||||
|
val formattedRemaining: String
|
||||||
|
get() {
|
||||||
|
val total = remainingSeconds.toInt().coerceAtLeast(0)
|
||||||
|
val h = total / 3600
|
||||||
|
val m = (total % 3600) / 60
|
||||||
|
val s = total % 60
|
||||||
|
return if (h > 0) String.format("%02d:%02d:%02d", h, m, s)
|
||||||
|
else String.format("%02d:%02d", m, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user