Commit 0842f3b34060b4180df0da1cdfd8d346e5e42e0e

Authored by yibo
1 parent deea171c

feat: 品牌重命名 Life OS + 跨源统一时间轴

- app 名称:Lifelog → Life OS(strings.xml + AndroidManifest 硬编码修正)
- 图标:红色关系网络 → 深紫渐变轨道设计(人物居中 + 双同心环 + 卫星节点)
- 新增 TimelineScreen:录音/微信/朋友圈/照片四源按时间混排,日期分隔头,来源过滤 chip
- 底部导航加「时间轴」tab(第三位:对话/人物/时间轴/录音/相册/设置)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lifelog-recorder/app/src/main/AndroidManifest.xml
... ... @@ -21,7 +21,7 @@
21 21  
22 22 <application
23 23 android:name=".LifelogApp"
24   - android:label="Lifelog"
  24 + android:label="@string/app_name"
25 25 android:icon="@mipmap/ic_launcher"
26 26 android:roundIcon="@mipmap/ic_launcher_round"
27 27 android:allowBackup="false"
... ... @@ -33,7 +33,7 @@
33 33 <activity
34 34 android:name=".MainActivity"
35 35 android:exported="true"
36   - android:label="Lifelog"
  36 + android:label="@string/app_name"
37 37 android:windowSoftInputMode="adjustResize"
38 38 android:theme="@style/Theme.Lifelog">
39 39 <intent-filter>
... ...
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/MainActivity.kt
... ... @@ -122,8 +122,9 @@ class MainActivity : ComponentActivity() {
122 122 }
123 123  
124 124 private enum class Tab(val label: String, val icon: String) {
125   - Chat("对话", "💬"), // 默认首页:AI 对话 + 工具模式
  125 + Chat("对话", "💬"),
126 126 People("人物", "👤"),
  127 + Timeline("时间轴", "🕐"), // Life OS 核心:跨源统一时间轴
127 128 Recording("录音", "⏺"),
128 129 Album("相册", "🖼"),
129 130 Settings("设置", "⚙"),
... ... @@ -175,6 +176,7 @@ private fun MainScreen() {
175 176 ) { inner ->
176 177 Box(modifier = Modifier.padding(inner)) {
177 178 when (tab) {
  179 + Tab.Timeline -> TimelineScreen()
178 180 Tab.People -> ContactsTab(repo)
179 181 Tab.Recording -> RecordingScreen(
180 182 activity = activity, repo = repo, speakers = speakers,
... ...
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/TimelineScreen.kt 0 → 100644
  1 +package com.lemon.lifelog
  2 +
  3 +import android.app.Application
  4 +import android.net.Uri
  5 +import androidx.compose.foundation.background
  6 +import androidx.compose.foundation.horizontalScroll
  7 +import androidx.compose.foundation.layout.Arrangement
  8 +import androidx.compose.foundation.layout.Box
  9 +import androidx.compose.foundation.layout.Column
  10 +import androidx.compose.foundation.layout.PaddingValues
  11 +import androidx.compose.foundation.layout.Row
  12 +import androidx.compose.foundation.layout.fillMaxSize
  13 +import androidx.compose.foundation.layout.fillMaxWidth
  14 +import androidx.compose.foundation.layout.height
  15 +import androidx.compose.foundation.layout.padding
  16 +import androidx.compose.foundation.layout.size
  17 +import androidx.compose.foundation.layout.width
  18 +import androidx.compose.foundation.lazy.LazyColumn
  19 +import androidx.compose.foundation.lazy.items
  20 +import androidx.compose.foundation.rememberScrollState
  21 +import androidx.compose.foundation.shape.CircleShape
  22 +import androidx.compose.foundation.shape.RoundedCornerShape
  23 +import androidx.compose.material3.FilterChip
  24 +import androidx.compose.material3.MaterialTheme
  25 +import androidx.compose.material3.Surface
  26 +import androidx.compose.material3.Text
  27 +import androidx.compose.runtime.Composable
  28 +import androidx.compose.runtime.collectAsState
  29 +import androidx.compose.runtime.getValue
  30 +import androidx.compose.runtime.mutableStateOf
  31 +import androidx.compose.runtime.remember
  32 +import androidx.compose.runtime.setValue
  33 +import androidx.compose.ui.Alignment
  34 +import androidx.compose.ui.Modifier
  35 +import androidx.compose.ui.draw.clip
  36 +import androidx.compose.ui.graphics.Color
  37 +import androidx.compose.ui.layout.ContentScale
  38 +import androidx.compose.ui.platform.LocalContext
  39 +import androidx.compose.ui.text.font.FontWeight
  40 +import androidx.compose.ui.text.style.TextOverflow
  41 +import androidx.compose.ui.unit.dp
  42 +import androidx.compose.ui.unit.sp
  43 +import androidx.lifecycle.AndroidViewModel
  44 +import androidx.lifecycle.viewModelScope
  45 +import androidx.lifecycle.viewmodel.compose.viewModel
  46 +import coil.compose.AsyncImage
  47 +import coil.request.ImageRequest
  48 +import com.lemon.lifelog.data.Moment
  49 +import com.lemon.lifelog.data.Person
  50 +import com.lemon.lifelog.data.PhotoRow
  51 +import com.lemon.lifelog.data.Repository
  52 +import com.lemon.lifelog.data.SegmentRow
  53 +import com.lemon.lifelog.data.WechatMessage
  54 +import kotlinx.coroutines.flow.SharingStarted
  55 +import kotlinx.coroutines.flow.combine
  56 +import kotlinx.coroutines.flow.stateIn
  57 +import java.io.File
  58 +import java.text.SimpleDateFormat
  59 +import java.util.Date
  60 +import java.util.Locale
  61 +
  62 +// ─── 数据模型 ───────────────────────────────────────────────────────────────
  63 +
  64 +sealed class TimelineItem {
  65 + abstract val ts: Long
  66 +
  67 + data class Voice(val segId: Long, override val ts: Long,
  68 + val personName: String?, val text: String) : TimelineItem()
  69 +
  70 + data class WeChat(val id: Long, override val ts: Long,
  71 + val personName: String?, val content: String,
  72 + val isSend: Boolean) : TimelineItem()
  73 +
  74 + data class Moment(val id: Long, override val ts: Long,
  75 + val personName: String?, val text: String) : TimelineItem()
  76 +
  77 + data class Photo(val id: Long, override val ts: Long,
  78 + val path: String, val caption: String?,
  79 + val faceCount: Int) : TimelineItem()
  80 +}
  81 +
  82 +enum class TlSource(val label: String, val icon: String) {
  83 + VOICE("录音", "🎙"),
  84 + WECHAT("微信", "💬"),
  85 + MOMENT("朋友圈", "👥"),
  86 + PHOTO("照片", "📷"),
  87 +}
  88 +
  89 +// ─── ViewModel ──────────────────────────────────────────────────────────────
  90 +
  91 +class TimelineViewModel(app: Application) : AndroidViewModel(app) {
  92 + private val repo = Repository(app)
  93 +
  94 + val items = combine(
  95 + repo.segments.observeTimeline(300),
  96 + repo.wechatMessages.observeRecent(300),
  97 + repo.moments.observeRecent(300),
  98 + repo.photos.observeGrid(300),
  99 + repo.persons.observeAll(),
  100 + ) { segs: List<SegmentRow>, msgs: List<WechatMessage>, moments: List<Moment>,
  101 + photos: List<PhotoRow>, persons: List<Person> ->
  102 +
  103 + val wxMap = persons.mapNotNull { p ->
  104 + val ref = p.contactRef ?: return@mapNotNull null
  105 + if (!ref.startsWith("wechat:")) return@mapNotNull null
  106 + ref.removePrefix("wechat:") to p.name
  107 + }.toMap()
  108 +
  109 + buildList {
  110 + segs.forEach { s ->
  111 + val t = s.text?.takeIf { it.isNotBlank() } ?: return@forEach
  112 + add(TimelineItem.Voice(s.id, s.startTs, s.speakerName ?: s.personName, t))
  113 + }
  114 + msgs.forEach { m ->
  115 + if (m.content.isBlank()) return@forEach
  116 + add(TimelineItem.WeChat(m.id, m.createTime, wxMap[m.talker], m.content, m.isSend))
  117 + }
  118 + moments.forEach { m ->
  119 + if (m.text.isBlank()) return@forEach
  120 + add(TimelineItem.Moment(m.id, m.createTime, wxMap[m.userName] ?: m.userName, m.text))
  121 + }
  122 + photos.filter { it.scanState >= 1 || it.faceCount > 0 || !it.caption.isNullOrBlank() }
  123 + .forEach { p ->
  124 + add(TimelineItem.Photo(p.id, p.takenTs, p.path, p.caption, p.faceCount))
  125 + }
  126 + }.sortedByDescending { it.ts }
  127 + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
  128 +}
  129 +
  130 +// ─── 辅助:带日期分隔头的扁平列表 ─────────────────────────────────────────
  131 +
  132 +private sealed class TlEntry {
  133 + data class Header(val label: String) : TlEntry()
  134 + data class Event(val item: TimelineItem) : TlEntry()
  135 +}
  136 +
  137 +private val dayFmt = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
  138 +private val lblFmt = SimpleDateFormat("M月d日", Locale.CHINESE)
  139 +private val timeFmt = SimpleDateFormat("HH:mm", Locale.getDefault())
  140 +
  141 +private fun buildEntries(items: List<TimelineItem>): List<TlEntry> {
  142 + val result = mutableListOf<TlEntry>()
  143 + var lastDay = ""
  144 + val today = dayFmt.format(System.currentTimeMillis())
  145 + val yesterday = dayFmt.format(System.currentTimeMillis() - 86_400_000L)
  146 + for (item in items) {
  147 + val day = dayFmt.format(Date(item.ts))
  148 + if (day != lastDay) {
  149 + result += TlEntry.Header(when (day) {
  150 + today -> "今天"
  151 + yesterday -> "昨天"
  152 + else -> lblFmt.format(Date(item.ts))
  153 + })
  154 + lastDay = day
  155 + }
  156 + result += TlEntry.Event(item)
  157 + }
  158 + return result
  159 +}
  160 +
  161 +// ─── Screen ─────────────────────────────────────────────────────────────────
  162 +
  163 +@Composable
  164 +fun TimelineScreen() {
  165 + val vm: TimelineViewModel = viewModel()
  166 + val allItems by vm.items.collectAsState()
  167 +
  168 + var activeFilters by remember { mutableStateOf(TlSource.entries.toSet()) }
  169 +
  170 + val filtered = remember(allItems, activeFilters) {
  171 + allItems.filter { item ->
  172 + when (item) {
  173 + is TimelineItem.Voice -> TlSource.VOICE in activeFilters
  174 + is TimelineItem.WeChat -> TlSource.WECHAT in activeFilters
  175 + is TimelineItem.Moment -> TlSource.MOMENT in activeFilters
  176 + is TimelineItem.Photo -> TlSource.PHOTO in activeFilters
  177 + }
  178 + }
  179 + }
  180 +
  181 + val entries = remember(filtered) { buildEntries(filtered) }
  182 +
  183 + Column(Modifier.fillMaxSize()) {
  184 + // 过滤 chips
  185 + Row(
  186 + modifier = Modifier
  187 + .horizontalScroll(rememberScrollState())
  188 + .padding(horizontal = 12.dp, vertical = 6.dp),
  189 + horizontalArrangement = Arrangement.spacedBy(8.dp),
  190 + ) {
  191 + val allOn = activeFilters.size == TlSource.entries.size
  192 + FilterChip(
  193 + selected = allOn,
  194 + onClick = {
  195 + activeFilters =
  196 + if (allOn) emptySet() else TlSource.entries.toSet()
  197 + },
  198 + label = { Text("全部") },
  199 + )
  200 + TlSource.entries.forEach { src ->
  201 + FilterChip(
  202 + selected = src in activeFilters,
  203 + onClick = {
  204 + activeFilters =
  205 + if (src in activeFilters) activeFilters - src
  206 + else activeFilters + src
  207 + },
  208 + label = { Text("${src.icon} ${src.label}") },
  209 + )
  210 + }
  211 + }
  212 +
  213 + if (entries.isEmpty()) {
  214 + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
  215 + Text("暂无记录", color = MaterialTheme.colorScheme.onSurfaceVariant)
  216 + }
  217 + } else {
  218 + LazyColumn(
  219 + modifier = Modifier.fillMaxSize(),
  220 + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
  221 + verticalArrangement = Arrangement.spacedBy(2.dp),
  222 + ) {
  223 + items(
  224 + entries,
  225 + key = { e ->
  226 + when (e) {
  227 + is TlEntry.Header -> "h_${e.label}"
  228 + is TlEntry.Event -> when (val i = e.item) {
  229 + is TimelineItem.Voice -> "v_${i.segId}"
  230 + is TimelineItem.WeChat -> "w_${i.id}"
  231 + is TimelineItem.Moment -> "mo_${i.id}"
  232 + is TimelineItem.Photo -> "p_${i.id}"
  233 + }
  234 + }
  235 + },
  236 + ) { entry ->
  237 + when (entry) {
  238 + is TlEntry.Header -> TlDateHeader(entry.label)
  239 + is TlEntry.Event -> TlEventRow(entry.item)
  240 + }
  241 + }
  242 + }
  243 + }
  244 + }
  245 +}
  246 +
  247 +// ─── 子组件 ──────────────────────────────────────────────────────────────────
  248 +
  249 +@Composable
  250 +private fun TlDateHeader(label: String) {
  251 + Text(
  252 + text = label,
  253 + style = MaterialTheme.typography.labelMedium,
  254 + color = MaterialTheme.colorScheme.onSurfaceVariant,
  255 + modifier = Modifier
  256 + .fillMaxWidth()
  257 + .padding(top = 14.dp, bottom = 4.dp, start = 4.dp),
  258 + )
  259 +}
  260 +
  261 +private data class SrcStyle(val icon: String, val color: Color)
  262 +
  263 +private fun srcStyle(item: TimelineItem) = when (item) {
  264 + is TimelineItem.Voice -> SrcStyle("🎙", Color(0xFF7C4DFF))
  265 + is TimelineItem.WeChat -> SrcStyle("💬", Color(0xFF43A047))
  266 + is TimelineItem.Moment -> SrcStyle("👥", Color(0xFF1E88E5))
  267 + is TimelineItem.Photo -> SrcStyle("📷", Color(0xFFFF9800))
  268 +}
  269 +
  270 +@Composable
  271 +private fun TlEventRow(item: TimelineItem) {
  272 + val (icon, color) = srcStyle(item)
  273 + Row(
  274 + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
  275 + horizontalArrangement = Arrangement.spacedBy(8.dp),
  276 + verticalAlignment = Alignment.Top,
  277 + ) {
  278 + // 左侧:图标 + 时间
  279 + Column(
  280 + horizontalAlignment = Alignment.CenterHorizontally,
  281 + modifier = Modifier.width(44.dp).padding(top = 2.dp),
  282 + ) {
  283 + Box(
  284 + Modifier
  285 + .size(28.dp)
  286 + .background(color.copy(alpha = 0.15f), CircleShape),
  287 + contentAlignment = Alignment.Center,
  288 + ) {
  289 + Text(icon, fontSize = 14.sp)
  290 + }
  291 + Text(
  292 + timeFmt.format(Date(item.ts)),
  293 + style = MaterialTheme.typography.labelSmall,
  294 + color = MaterialTheme.colorScheme.onSurfaceVariant,
  295 + modifier = Modifier.padding(top = 2.dp),
  296 + )
  297 + }
  298 +
  299 + // 右侧:内容卡片
  300 + Surface(
  301 + modifier = Modifier.weight(1f),
  302 + shape = RoundedCornerShape(8.dp),
  303 + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f),
  304 + ) {
  305 + Column(Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) {
  306 + // 人名标签
  307 + val name: String? = when (item) {
  308 + is TimelineItem.Voice -> item.personName
  309 + is TimelineItem.WeChat -> if (item.isSend) "我" else item.personName
  310 + is TimelineItem.Moment -> item.personName
  311 + is TimelineItem.Photo -> null
  312 + }
  313 + if (name != null) {
  314 + Text(
  315 + name,
  316 + style = MaterialTheme.typography.labelSmall,
  317 + color = color,
  318 + fontWeight = FontWeight.SemiBold,
  319 + modifier = Modifier.padding(bottom = 2.dp),
  320 + )
  321 + }
  322 +
  323 + // 正文
  324 + when (item) {
  325 + is TimelineItem.Voice -> Text(
  326 + item.text,
  327 + style = MaterialTheme.typography.bodySmall,
  328 + maxLines = 2,
  329 + overflow = TextOverflow.Ellipsis,
  330 + )
  331 + is TimelineItem.WeChat -> Text(
  332 + item.content,
  333 + style = MaterialTheme.typography.bodySmall,
  334 + maxLines = 2,
  335 + overflow = TextOverflow.Ellipsis,
  336 + )
  337 + is TimelineItem.Moment -> Text(
  338 + item.text,
  339 + style = MaterialTheme.typography.bodySmall,
  340 + maxLines = 3,
  341 + overflow = TextOverflow.Ellipsis,
  342 + )
  343 + is TimelineItem.Photo -> {
  344 + val ctx = LocalContext.current
  345 + AsyncImage(
  346 + model = ImageRequest.Builder(ctx)
  347 + .data(
  348 + if (item.path.startsWith("content://"))
  349 + Uri.parse(item.path)
  350 + else File(item.path)
  351 + )
  352 + .crossfade(true)
  353 + .size(600)
  354 + .build(),
  355 + contentDescription = null,
  356 + contentScale = ContentScale.Crop,
  357 + modifier = Modifier
  358 + .fillMaxWidth()
  359 + .height(96.dp)
  360 + .clip(RoundedCornerShape(4.dp)),
  361 + )
  362 + if (!item.caption.isNullOrBlank()) {
  363 + Text(
  364 + item.caption,
  365 + style = MaterialTheme.typography.bodySmall,
  366 + maxLines = 2,
  367 + overflow = TextOverflow.Ellipsis,
  368 + color = MaterialTheme.colorScheme.onSurfaceVariant,
  369 + modifier = Modifier.padding(top = 4.dp),
  370 + )
  371 + }
  372 + }
  373 + }
  374 + }
  375 + }
  376 + }
  377 +}
... ...
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Daos.kt
... ... @@ -875,6 +875,10 @@ interface MomentDao {
875 875 /** 全库关键词搜索朋友圈文字。 */
876 876 @Query("SELECT * FROM moments WHERE text LIKE '%' || :q || '%' ORDER BY createTime DESC LIMIT :n")
877 877 suspend fun searchText(q: String, n: Int): List<Moment>
  878 +
  879 + /** 时间轴用:最近 n 条朋友圈(所有人混排)。 */
  880 + @Query("SELECT * FROM moments ORDER BY createTime DESC LIMIT :n")
  881 + fun observeRecent(n: Int): Flow<List<Moment>>
878 882 }
879 883  
880 884 @Dao
... ... @@ -911,6 +915,10 @@ interface WechatMessageDao {
911 915 ORDER BY createTime DESC LIMIT :n
912 916 """)
913 917 suspend fun searchGroup(q: String, n: Int): List<WechatMessage>
  918 +
  919 + /** 时间轴用:最近 n 条私聊(排除群聊)。 */
  920 + @Query("SELECT * FROM wechat_messages WHERE talker NOT LIKE '%@chatroom' ORDER BY createTime DESC LIMIT :n")
  921 + fun observeRecent(n: Int): Flow<List<WechatMessage>>
914 922 }
915 923  
916 924 @Dao
... ...
lifelog-recorder/app/src/main/res/drawable/ic_launcher_background.xml
1 1 <?xml version="1.0" encoding="utf-8"?>
2 2 <shape xmlns:android="http://schemas.android.com/apk/res/android">
3 3 <gradient
4   - android:startColor="#C62828"
5   - android:endColor="#FF5252"
  4 + android:startColor="#0F0A25"
  5 + android:endColor="#5B0EA6"
6 6 android:type="linear"
7 7 android:angle="315"/>
8 8 </shape>
... ...
lifelog-recorder/app/src/main/res/drawable/ic_launcher_foreground.xml
1 1 <?xml version="1.0" encoding="utf-8"?>
2 2 <!--
3   - 人际关系网络图标(108×108dp viewport,safe zone 18–90)
4   - 布局:中央大节点("我")+ 4个卫星节点(上/右/下/左),白色连线+节点,
5   - 中央节点内嵌红色人物轮廓(圆形头 + 肩线),卫星节点为纯白圆。
  3 + Life OS 图标(108×108dp viewport,safe zone 18–90)
  4 + 布局:中央白圆盘(紫色人物轮廓)+ 两圈同心轨道环(白色半透明)
  5 + + 对角两颗卫星节点(白色实心圆)
  6 + 隐喻:你是中心,Memory/Relationship/Goal/Execution 各层围绕你运转
6 7 -->
7 8 <vector xmlns:android="http://schemas.android.com/apk/res/android"
8 9 android:width="108dp"
... ... @@ -10,90 +11,40 @@
10 11 android:viewportWidth="108"
11 12 android:viewportHeight="108">
12 13  
13   - <!-- ── 连线(在节点层之下,白色半透明) ── -->
14   - <!-- 中央 → 上 -->
15   - <path android:pathData="M54,39 L54,31"
16   - android:strokeColor="#FFFFFF" android:strokeWidth="3.5"
17   - android:strokeAlpha="0.75" android:strokeLineCap="round"
18   - android:fillColor="#00000000"/>
19   - <!-- 中央 → 右 -->
20   - <path android:pathData="M69,54 L79,54"
21   - android:strokeColor="#FFFFFF" android:strokeWidth="3.5"
22   - android:strokeAlpha="0.75" android:strokeLineCap="round"
23   - android:fillColor="#00000000"/>
24   - <!-- 中央 → 下 -->
25   - <path android:pathData="M54,69 L54,77"
26   - android:strokeColor="#FFFFFF" android:strokeWidth="3.5"
27   - android:strokeAlpha="0.75" android:strokeLineCap="round"
28   - android:fillColor="#00000000"/>
29   - <!-- 中央 → 左 -->
30   - <path android:pathData="M39,54 L29,54"
31   - android:strokeColor="#FFFFFF" android:strokeWidth="3.5"
32   - android:strokeAlpha="0.75" android:strokeLineCap="round"
33   - android:fillColor="#00000000"/>
34   - <!-- 次级连线:上 → 右(让图更像真实网络) -->
35   - <path android:pathData="M62,23 L80,46"
36   - android:strokeColor="#FFFFFF" android:strokeWidth="2"
37   - android:strokeAlpha="0.45" android:strokeLineCap="round"
38   - android:fillColor="#00000000"/>
39   - <!-- 次级连线:下 → 左 -->
40   - <path android:pathData="M46,85 L28,62"
41   - android:strokeColor="#FFFFFF" android:strokeWidth="2"
42   - android:strokeAlpha="0.45" android:strokeLineCap="round"
  14 + <!-- 外轨道环 r=33,细描边,低透明度 -->
  15 + <path android:pathData="M21,54 A33,33 0 1,0 87,54 A33,33 0 1,0 21,54"
  16 + android:strokeColor="#FFFFFF"
  17 + android:strokeWidth="2.5"
  18 + android:strokeAlpha="0.55"
43 19 android:fillColor="#00000000"/>
44 20  
45   - <!-- ── 卫星节点(白色实心圆,r=10) ── -->
46   - <!-- 上 -->
47   - <path android:pathData="M54,21 m-10,0 a10,10 0 1,0 20,0 a10,10 0 1,0 -20,0"
48   - android:fillColor="#FFFFFF"/>
49   - <!-- 右 -->
50   - <path android:pathData="M89,54 m-10,0 a10,10 0 1,0 20,0 a10,10 0 1,0 -20,0"
51   - android:fillColor="#FFFFFF"/>
52   - <!-- 下 -->
53   - <path android:pathData="M54,87 m-10,0 a10,10 0 1,0 20,0 a10,10 0 1,0 -20,0"
54   - android:fillColor="#FFFFFF"/>
55   - <!-- 左 -->
56   - <path android:pathData="M19,54 m-10,0 a10,10 0 1,0 20,0 a10,10 0 1,0 -20,0"
57   - android:fillColor="#FFFFFF"/>
58   -
59   - <!-- 卫星节点内小人(头+肩,红色)以"上"节点为例,其他类似 -->
60   - <!-- 上节点人物头 -->
61   - <path android:pathData="M54,18 m-3.5,0 a3.5,3.5 0 1,0 7,0 a3.5,3.5 0 1,0 -7,0"
62   - android:fillColor="#C62828"/>
63   - <!-- 上节点人物肩 -->
64   - <path android:pathData="M49,25 C49,22.5 51.2,21 54,21 C56.8,21 59,22.5 59,25 Z"
65   - android:fillColor="#C62828"/>
66   -
67   - <!-- 右节点人物头 -->
68   - <path android:pathData="M89,51 m-3.5,0 a3.5,3.5 0 1,0 7,0 a3.5,3.5 0 1,0 -7,0"
69   - android:fillColor="#C62828"/>
70   - <!-- 右节点人物肩 -->
71   - <path android:pathData="M84,58 C84,55.5 86.2,54 89,54 C91.8,54 94,55.5 94,58 Z"
72   - android:fillColor="#C62828"/>
  21 + <!-- 内轨道环 r=22,更细,更低透明度 -->
  22 + <path android:pathData="M32,54 A22,22 0 1,0 76,54 A22,22 0 1,0 32,54"
  23 + android:strokeColor="#FFFFFF"
  24 + android:strokeWidth="2"
  25 + android:strokeAlpha="0.35"
  26 + android:fillColor="#00000000"/>
73 27  
74   - <!-- 下节点人物头 -->
75   - <path android:pathData="M54,84 m-3.5,0 a3.5,3.5 0 1,0 7,0 a3.5,3.5 0 1,0 -7,0"
76   - android:fillColor="#C62828"/>
77   - <!-- 下节点人物肩 -->
78   - <path android:pathData="M49,91 C49,88.5 51.2,87 54,87 C56.8,87 59,88.5 59,91 Z"
79   - android:fillColor="#C62828"/>
  28 + <!-- 卫星节点:右上方(约 77,31),在外轨道 45° 位置 -->
  29 + <path android:pathData="M72,31 A5,5 0 1,0 82,31 A5,5 0 1,0 72,31"
  30 + android:fillColor="#FFFFFF"
  31 + android:fillAlpha="0.9"/>
80 32  
81   - <!-- 左节点人物头 -->
82   - <path android:pathData="M19,51 m-3.5,0 a3.5,3.5 0 1,0 7,0 a3.5,3.5 0 1,0 -7,0"
83   - android:fillColor="#C62828"/>
84   - <!-- 左节点人物肩 -->
85   - <path android:pathData="M14,58 C14,55.5 16.2,54 19,54 C21.8,54 24,55.5 24,58 Z"
86   - android:fillColor="#C62828"/>
  33 + <!-- 卫星节点:左下方(约 31,77),在外轨道 225° 位置 -->
  34 + <path android:pathData="M26,77 A5,5 0 1,0 36,77 A5,5 0 1,0 26,77"
  35 + android:fillColor="#FFFFFF"
  36 + android:fillAlpha="0.9"/>
87 37  
88   - <!-- ── 中央大节点(白色实心圆,r=15) ── -->
89   - <path android:pathData="M54,54 m-15,0 a15,15 0 1,0 30,0 a15,15 0 1,0 -30,0"
  38 + <!-- 中央白色圆盘 r=13 -->
  39 + <path android:pathData="M41,54 A13,13 0 1,0 67,54 A13,13 0 1,0 41,54"
90 40 android:fillColor="#FFFFFF"/>
91 41  
92   - <!-- 中央节点内人物头(红色,r=5.5) -->
93   - <path android:pathData="M54,49 m-5.5,0 a5.5,5.5 0 1,0 11,0 a5.5,5.5 0 1,0 -11,0"
94   - android:fillColor="#C62828"/>
95   - <!-- 中央节点内人物肩(红色) -->
96   - <path android:pathData="M44,62 C44,57.5 48.5,54.5 54,54.5 C59.5,54.5 64,57.5 64,62 Z"
97   - android:fillColor="#C62828"/>
  42 + <!-- 人物头像(紫色,r=4,中心 54,49)—— 在白圆盘内显示为实色 -->
  43 + <path android:pathData="M50,49 A4,4 0 1,0 58,49 A4,4 0 1,0 50,49"
  44 + android:fillColor="#5B0EA6"/>
  45 +
  46 + <!-- 人物肩膀(紫色曲线,弧形填充) -->
  47 + <path android:pathData="M46,62 C46,57.5 49.8,55 54,55 C58.2,55 62,57.5 62,62 Z"
  48 + android:fillColor="#5B0EA6"/>
98 49  
99 50 </vector>
... ...
lifelog-recorder/app/src/main/res/values/strings.xml
1 1 <?xml version="1.0" encoding="utf-8"?>
2 2 <resources>
3   - <string name="app_name">Lifelog</string>
  3 + <string name="app_name">Life OS</string>
4 4 </resources>
... ...