Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ and rule traces back to a section (and often a community demand source) there.
| Brick tags are programmed on pairing (LifeOS MIME record + Android Application Record), so a tap flips a mode with the app closed and without a chooser; broader NDEF/TECH/TAG filters, tag id read from the record, and an NFC-off banner that opens NFC settings | Done |
| Brick inverse mode: pick the window you want socials shut (e.g. 08:00-22:00) and the tag becomes the way in, not out - each tap buys a set stretch of access (15/30/60/120 min or a typed value), capped at one, two, three or unlimited unlocks per window, with the count resetting when the window next starts. Outside the window nothing is blocked. Covered by unit tests | Done |
| One motion vocabulary app-wide (core:designsystem/Motion.kt): screens fade through each other with a barely-there scale, swapped content cross-fades (clock and Pastebin tabs, Brick list/editor, screen-time week/day stats, timer wheels/typed entry), banners fade in, and resizing layouts animate instead of snapping | Done |
| Motion slowed and simplified: fades run 420 ms in, 260 ms out, and every scale/corner-reveal is gone (screen transitions, the quick-capture button, the Vault reveal are pure cross-fades) | Done |
| Jarvis reaches every module: each feature registers a read provider and action handler through core:service, so he can pull screen-time numbers, plants, Brick state, headlines, the stargazing forecast, downloads, pastes and focus state, and act - create pastes (including one-time encrypted ones), queue downloads, water plants, start/stop Brick modes, run the focus timer, sync or export screen time, run macros. Detail is fetched on demand with `[[get: topic]]` and answered in a second pass, so the always-on prompt stays small | Done |
| Jarvis takes images: attach photos from the gallery, on-device Gemma reads them through a vision session (MediaPipe image modality) and NAS Ollama gets them as base64 - the attachment and the reply both stay in the thread | Done |
| Macros overhauled: build macros by hand with a "+" and a searchable action picker, edit anything (AI-written or not), reorder or delete steps, and test without saving. 50+ actions across apps, navigation, screen, timing, device and LifeOS itself (open app/link/settings, tap by text/description/view id, long-press, type, clear, scroll, four swipes, wait-for-text, media keys, volume, torch, vibrate, clipboard, share, toast, plus LifeOS tasks/notes/timers/focus/Brick). AI compilation now sees the installed app list, is told to use the fewest steps, and its output is normalised ("Spotify App" becomes Spotify) and opened in the editor for review | Done |
| Focus timer keeps running: state lives in a singleton driven by an absolute deadline, so switching tabs, going Home or leaving the app no longer resets it. The time in the ring is edited in place (no dialog), and the overlay can be dragged anywhere and pinched to resize; closing it with its X now updates the in-app button | Done |
| Pastebin honesty pass: its developer API has neither burn-after-read nor paste passwords, so those requests now go to PrivateBin instead - AES-256-GCM encrypted on the phone, key in the link fragment, verified against a live instance. Maintenance pages and bad logins are reported instead of being mistaken for success, and signed-in pastes land in the account | Done |
| Home tiles auto-scroll while you drag one near the top or bottom edge | Done |
| Deferred post-alpha: Glance home-screen widgets, HA WebSocket live state/zones, Vault unlock UI, first-run onboarding checklist (grants live in Settings → System access), FinTS bank sync | Planned |

**Google-free by design:** no Google service is ever called at runtime (no Play Services, no Google recognizer, no Google Maps). Remaining Google-*authored* open-source, fully on-device libraries: AndroidX/Jetpack (unavoidable on Android), MediaPipe (Gemma inference), ML Kit on-device OCR/barcode (no network) — swap candidates documented in the plan.
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {

defaultConfig {
applicationId = "com.lifeos"
versionCode = 21
versionName = "0.1.0-alpha.21"
versionCode = 22
versionName = "0.1.0-alpha.22"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
17 changes: 5 additions & 12 deletions app/src/main/kotlin/com/lifeos/app/ui/LifeOsApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import com.lifeos.app.ui.settings.SettingsRoute
import com.lifeos.core.ui.navigation.LifeDestination
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import com.lifeos.core.designsystem.component.FadeVisible
import com.lifeos.core.designsystem.component.LifeMotion
import com.lifeos.core.ui.navigation.TopLevelDestination
import com.lifeos.feature.adhd.FocusRoute
Expand Down Expand Up @@ -98,11 +98,7 @@ fun LifeOsApp(captureRequests: Int = 0, navBarIds: List<String> = emptyList()) {
modifier = Modifier.fillMaxSize(),
floatingActionButton = {
// Quick capture lives on Home only; feature screens own their create FABs.
androidx.compose.animation.AnimatedVisibility(
visible = currentDestination?.hasRoute(LifeDestination.Home::class) == true,
enter = androidx.compose.animation.scaleIn() + androidx.compose.animation.fadeIn(),
exit = androidx.compose.animation.scaleOut() + androidx.compose.animation.fadeOut(),
) {
FadeVisible(visible = currentDestination?.hasRoute(LifeDestination.Home::class) == true) {
FloatingActionButton(onClick = { showQuickCapture = true }) {
Icon(Icons.Filled.Bolt, contentDescription = "Quick capture")
}
Expand Down Expand Up @@ -141,13 +137,10 @@ fun LifeOsApp(captureRequests: Int = 0, navBarIds: List<String> = emptyList()) {
modifier = Modifier.padding(innerPadding),
// One motion vocabulary (§7): screens fade through each other with a
// barely-there scale, nothing slides or bounces.
enterTransition = {
fadeIn(LifeMotion.enterSpec()) + scaleIn(LifeMotion.enterSpec(), initialScale = 0.98f)
},
// Pure cross-fades: any scale reads as a corner reveal on a phone.
enterTransition = { fadeIn(LifeMotion.enterSpec()) },
exitTransition = { fadeOut(LifeMotion.exitSpec()) },
popEnterTransition = {
fadeIn(LifeMotion.enterSpec()) + scaleIn(LifeMotion.enterSpec(), initialScale = 1.01f)
},
popEnterTransition = { fadeIn(LifeMotion.enterSpec()) },
popExitTransition = { fadeOut(LifeMotion.exitSpec()) },
) {
composable<LifeDestination.Home> {
Expand Down
99 changes: 66 additions & 33 deletions app/src/main/kotlin/com/lifeos/app/ui/screen/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -1,61 +1,63 @@
package com.lifeos.app.ui.screen

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.NightsStay
import androidx.compose.material.icons.automirrored.filled.Note
import androidx.compose.material.icons.filled.Insights
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.automirrored.filled.Note
import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.material.icons.filled.AccountBalanceWallet
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Navigation
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.DocumentScanner
import androidx.compose.material.icons.filled.LocalShipping
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material.icons.filled.Timeline
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.DocumentScanner
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Insights
import androidx.compose.material.icons.filled.LocalFlorist
import androidx.compose.material.icons.filled.LocalShipping
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Navigation
import androidx.compose.material.icons.filled.Newspaper
import androidx.compose.material.icons.filled.NightsStay
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Shield
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.Timelapse
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import kotlinx.coroutines.withTimeoutOrNull
import androidx.compose.material3.ListItem
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.filled.Timeline
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import com.lifeos.core.designsystem.component.smoothSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
Expand All @@ -65,11 +67,13 @@ import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.zIndex
import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.lifeos.core.designsystem.component.FadeVisible
import com.lifeos.core.designsystem.component.smoothSize
import com.lifeos.core.ui.navigation.LifeDestination
import com.lifeos.feature.planner.PlannerViewModel
import kotlinx.coroutines.withTimeoutOrNull

private data class AppGridItem(
val label: String,
Expand Down Expand Up @@ -262,11 +266,7 @@ fun HomeScreen(
}
},
)
androidx.compose.animation.AnimatedVisibility(
visible = vaultRevealed,
enter = androidx.compose.animation.scaleIn() + androidx.compose.animation.fadeIn(),
exit = androidx.compose.animation.scaleOut() + androidx.compose.animation.fadeOut(),
) {
FadeVisible(visible = vaultRevealed) {
IconButton(onClick = { onNavigate(LifeDestination.Vault) }) {
Icon(Icons.Filled.Lock, contentDescription = "Vault", tint = MaterialTheme.colorScheme.primary)
}
Expand Down Expand Up @@ -349,6 +349,24 @@ private fun ReorderableTileGrid(
IntRect(info.offset, info.size).contains(position.round())
}

// Auto-scroll while a tile is held near the top or bottom edge, so a long
// reorder does not need drag-release-drag again.
var edgeScroll by remember { mutableFloatStateOf(0f) }
LaunchedEffect(draggingKey) {
if (draggingKey == null) {
edgeScroll = 0f
return@LaunchedEffect
}
while (draggingKey != null) {
if (edgeScroll != 0f) {
gridState.scrollBy(edgeScroll)
// Keep the tile pinned under the finger while the list moves.
dragOffset += Offset(0f, edgeScroll)
}
withFrameNanos { }
}
}

LazyVerticalGrid(
state = gridState,
columns = if (listLayout) GridCells.Fixed(1) else GridCells.Adaptive(minSize = 160.dp),
Expand All @@ -366,6 +384,16 @@ private fun ReorderableTileGrid(
onDrag = { change, amount ->
change.consume()
dragOffset += amount
// Distance from the viewport edges decides the scroll speed.
val viewportHeight = gridState.layoutInfo.viewportSize.height.toFloat()
val pointerY = change.position.y
val zone = (viewportHeight * 0.18f).coerceAtLeast(72f)
edgeScroll = when {
pointerY < zone -> -((zone - pointerY) / zone) * MAX_EDGE_SCROLL
pointerY > viewportHeight - zone ->
((pointerY - (viewportHeight - zone)) / zone) * MAX_EDGE_SCROLL
else -> 0f
}
val key = draggingKey ?: return@detectDragGesturesAfterLongPress
val dragged = gridState.layoutInfo.visibleItemsInfo
.firstOrNull { it.key == key } ?: return@detectDragGesturesAfterLongPress
Expand All @@ -390,11 +418,13 @@ private fun ReorderableTileGrid(
onDragEnd = {
draggingKey = null
dragOffset = Offset.Zero
edgeScroll = 0f
onOrderChanged(order.map { it.label })
},
onDragCancel = {
draggingKey = null
dragOffset = Offset.Zero
edgeScroll = 0f
},
)
},
Expand Down Expand Up @@ -442,3 +472,6 @@ private fun ReorderableTileGrid(
}
}
}

/** Pixels per frame at the very edge of the viewport while reordering tiles. */
private const val MAX_EDGE_SCROLL = 18f
1 change: 1 addition & 0 deletions core/ai/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.kotlinx.serialization.json)
implementation(libs.mediapipe.tasks.genai)
implementation(libs.mediapipe.tasks.vision)
testImplementation(libs.turbine)
}
Loading
Loading