diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bb1abef --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,53 @@ +name: Build & Release APK +on: + pull_request: + branches: [main] + paths: + - "**/*.kt" + - "**/*.xml" + - "**/*.gradle.kts" + - "gradle/libs.versions.toml" + - "gradle/wrapper/**" + - "gradle.properties" + workflow_dispatch: +permissions: + contents: write +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check version is new + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION=$(grep 'versionName' app/build.gradle.kts | grep -oP '"[^"]+"' | tr -d '"') + echo "VERSION=${VERSION}" >> $GITHUB_ENV + if gh release view "v${VERSION}" &>/dev/null; then + echo "::error::v${VERSION} already released." + exit 1 + fi + - uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: '8.13' + cache-read-only: false + - run: echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties + - run: gradle :app:assembleDebug --no-daemon + - run: gradle :app:test --no-daemon + - run: gradle :app:lintDebug --no-daemon + - name: Create GitHub Release + if: github.event_name == 'workflow_dispatch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + APK_NAME="Dash-${{ env.VERSION }}-debug.apk" + mv app/build/outputs/apk/debug/app-debug.apk \ + app/build/outputs/apk/debug/${APK_NAME} + gh release create "v${{ env.VERSION }}" \ + "app/build/outputs/apk/debug/${APK_NAME}" \ + --title "Dash v${{ env.VERSION }}" \ + --notes "Debug build — enable 'Install unknown apps' in Android settings to sideload." diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml new file mode 100644 index 0000000..c4034b5 --- /dev/null +++ b/.github/workflows/changelog-check.yml @@ -0,0 +1,27 @@ +name: Changelog bump check +on: + pull_request: + branches: [main] + paths: + - "**/*.kt" + - "**/*.xml" + - "**/*.gradle.kts" + - "gradle/libs.versions.toml" +permissions: + contents: read +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fail if CHANGELOG.md was not updated + run: | + CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + if echo "$CHANGED" | grep -q "^CHANGELOG.md$"; then + echo "CHANGELOG.md updated — good." + else + echo "::error::Code changed but CHANGELOG.md was not updated." + exit 1 + fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..667b219 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: "CodeQL" +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + schedule: + - cron: "37 19 * * 2" +permissions: + security-events: write + actions: read + contents: read +jobs: + analyze: + name: Analyze (java-kotlin) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v4 + with: + languages: java-kotlin + queries: security-extended + - uses: actions/setup-java@v4 + with: + java-version: "17" + distribution: temurin + - uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: '8.13' + cache-read-only: false + - run: echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties + - run: gradle :app:assembleDebug --no-daemon + - uses: github/codeql-action/analyze@v4 + with: + category: /language:java-kotlin diff --git a/.github/workflows/license-sync.yml b/.github/workflows/license-sync.yml new file mode 100644 index 0000000..01aedac --- /dev/null +++ b/.github/workflows/license-sync.yml @@ -0,0 +1,24 @@ +name: Licence screen sync check +on: + pull_request: + branches: [main] + paths: + - "gradle/libs.versions.toml" +permissions: + contents: read +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fail if LicensesScreen.kt was not updated + run: | + CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + if echo "$CHANGED" | grep -q "LicensesScreen.kt"; then + echo "LicensesScreen.kt updated — good." + else + echo "::error::gradle/libs.versions.toml changed but LicensesScreen.kt was not updated." + exit 1 + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b1b4c..bb9c8f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,40 @@ # Changelog -All notable changes to the Android app are documented here. +All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ---- - ## [Unreleased] +### Fixed +- CI: provision Gradle 8.13 directly via `gradle/actions/setup-gradle` instead of + relying on `gradle-wrapper.jar`, which cannot be committed through the GitHub API +- Compose BOM bumped to 2024.09.00 (Material3 1.3.0) to gain `PullToRefreshBox` + and align `ModalBottomSheetProperties` signature (removed `securePolicy` param) +- Added missing `lifecycle-runtime-compose` dependency for `collectAsStateWithLifecycle` +- `TaskRepository.pendingReminders`: replaced unavailable `isNull()` DSL call with + a Kotlin-side `completedAt == null` filter +- `EditTaskSheet`: fixed `SecureFlagPolicy` import (`material3` → `compose.ui.window`) +- `SettingsViewModel.clearSaveError`: fixed assignment-as-expression syntax error +- Removed custom `debug.keystore` signing config; CI now uses the default Android + debug keystore so packaging does not fail in a clean environment +- `menuAnchor()` calls updated to `menuAnchor(MenuAnchorType)` in `EditChoreSheet`, + `SettingsScreen`, and `EditTaskSheet` — Material3 1.3.0 deprecated the + parameterless overload at `DeprecationLevel.ERROR` + ## [1.0.0] — 2026-05-30 ### Added -- choreDash tab: chore list with staleness indicators, NFC tap-to-log, - swipe-to-log with undo, group-by-category, archive/unarchive -- taskDash tab: task list with priority/due/created sort, filter chips - (All / Active / Done), group-by-category, collapsible done section, - task reminders via AlarmManager (exact alarms) -- Settings tab: Supabase URL + anon-key entry, owner selection, - light/dark/system theme toggle +- Initial Android app combining choreDash (NFC chore tracker) and taskDash + (shared to-do list), connecting to the same Supabase project as the web app +- choreDash tab: chore list with staleness colour bars, NFC tap-to-log, + swipe-to-log with undo snackbar, group-by-category, archive/unarchive +- taskDash tab: task list with priority/due/created sort, All/Active/Done + filter chips, group-by-category, collapsible done section, per-task + AlarmManager reminders, owner filter +- Settings tab: Supabase URL + anon-key, owner dropdown, light/dark/system + theme toggle, open-source licenses screen - NFC foreground dispatch in MainActivity; NDEF text/URI/raw-hex tag-ID extraction -- DailyStaleChoreWorker: once-a-day notification summarising overdue chores -- BootWorker: re-schedules pending task reminders after device reboot -- Open-source licenses screen -- Material3 dynamic-color theming seeded from #4A7C59 (sage green) +- DailyStaleChoreWorker (WorkManager periodic): daily overdue-chore notification +- BootWorker: re-schedules pending task reminders after reboot +- Material3 theme seeded from `#4A7C59` (sage green) +- CI workflows: build + release APK, changelog check, license-sync check, CodeQL diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f3a415 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# choreDash + taskDash — Android + +Native Android app combining two household tools: + +- **choreDash** — chore tracker with NFC tag support. Tap a tag on the fridge, washing machine, etc. to log that chore as done. +- **taskDash** — shared to-do list with categories, priority, due dates, and per-task reminder notifications. + +Both tools read from / write to the same Supabase project used by the web app. +Credentials are entered once in the **Settings** tab and persisted in DataStore. + +--- + +## Requirements + +| Tool | Version | +|------|---------| +| Android Studio | Hedgehog 2023.1+ | +| JDK | 17 | +| Android Gradle Plugin | 8.13.x | +| Compile SDK | 35 | +| Min SDK | 26 (Android 8) | + +--- + +## Build + +```bash +# Debug APK +./gradlew assembleDebug + +# Run unit tests +./gradlew test + +# Lint +./gradlew lintDebug +``` + +--- + +## Project structure + +``` +app/src/main/java/com/mapgie/dash/ + DashApplication.kt # HiltAndroidApp + WorkManager Configuration.Provider + MainActivity.kt # NFC foreground dispatch, Compose entry point + alarm/ + AlarmReceiver.kt # BroadcastReceiver for task reminders + AlarmScheduler.kt # AlarmManager wrapper + BootReceiver.kt # Re-schedules alarms after reboot + BootWorker.kt # HiltWorker: queries pending reminders on boot + DailyStaleChoreWorker.kt # HiltWorker: daily stale-chore notification + data/ + model/ # Chore.kt, Task.kt, Owner.kt + enums/extension fns + preferences/ # SettingsRepository (DataStore) + repository/ # ChoreRepository, TaskRepository (Supabase) + supabase/ # SupabaseClientProvider + di/ + AppModule.kt # Hilt modules + notification/ + NotificationHelper.kt # Channel creation + show helpers + nfc/ + NfcHandler.kt # NDEF/URI/raw-hex tag-ID extraction + ui/ + theme/ # Color.kt, Theme.kt, Type.kt + navigation/ # DashNavGraph.kt + screens/ + chores/ # ChoreListViewModel + ChoreListScreen + tasks/ # TaskListViewModel + TaskListScreen + settings/ # SettingsViewModel + SettingsScreen + licenses/ # LicensesScreen + components/ # ChoreCard, LogBottomSheet, EditChoreSheet, + # TaskCard, EditTaskSheet +``` + +--- + +## NFC setup + +1. Write NDEF Text records to your NFC tags (any NFC writer app). +2. The tag ID is used to identify a chore — see `NfcHandler.extractTagId()`. +3. When the app receives an NFC intent, `LogBottomSheet` opens pre-filled with the matching chore (or shows "Unknown tag" if no chore matches). + +--- + +## Supabase schema + +This app connects to the same Supabase project as the web app. +Expected tables: `chores`, `chore_logs`, `todos`, `owners`. + +--- + +## Binary files + +`app/debug.keystore` and `gradle/wrapper/gradle-wrapper.jar` are binary files. +After cloning, copy them from a local choreDash checkout or generate a new debug keystore with: + +```bash +keytool -genkey -v -keystore app/debug.keystore -alias androiddebugkey \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -storepass android -keypass android -dname "CN=Android Debug,O=Android,C=US" +``` + +--- + +## Open-source licenses + +See **Settings → Open-source licenses** inside the app. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d692459..ec60368 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,19 +20,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - signingConfigs { - named("debug") { - storeFile = file("debug.keystore") - storePassword = "android" - keyAlias = "androiddebugkey" - keyPassword = "android" - } - } - buildTypes { - debug { - signingConfig = signingConfigs.getByName("debug") - } release { isMinifyEnabled = true proguardFiles( @@ -60,6 +48,7 @@ android { dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 03822b3..c0eb0a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - @@ -9,10 +8,7 @@ - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - + + + - - - - + android:exported="false" tools:node="merge"> + - - - + \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/DashApplication.kt b/app/src/main/java/com/mapgie/dash/DashApplication.kt index d87fa41..352cea8 100644 --- a/app/src/main/java/com/mapgie/dash/DashApplication.kt +++ b/app/src/main/java/com/mapgie/dash/DashApplication.kt @@ -1,5 +1,4 @@ package com.mapgie.dash - import android.app.Application import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration @@ -14,26 +13,17 @@ import javax.inject.Inject @HiltAndroidApp class DashApplication : Application(), Configuration.Provider { - @Inject lateinit var workerFactory: HiltWorkerFactory - override val workManagerConfiguration: Configuration - get() = Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() - + get() = Configuration.Builder().setWorkerFactory(workerFactory).build() override fun onCreate() { super.onCreate() NotificationHelper.createChannels(this) scheduleDailyChoreCheck() } - private fun scheduleDailyChoreCheck() { val request = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS).build() WorkManager.getInstance(this).enqueueUniquePeriodicWork( - "daily_chore_check", - ExistingPeriodicWorkPolicy.KEEP, - request - ) + "daily_chore_check", ExistingPeriodicWorkPolicy.KEEP, request) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/MainActivity.kt b/app/src/main/java/com/mapgie/dash/MainActivity.kt index 80eced8..93c5953 100644 --- a/app/src/main/java/com/mapgie/dash/MainActivity.kt +++ b/app/src/main/java/com/mapgie/dash/MainActivity.kt @@ -107,4 +107,4 @@ class MainActivity : ComponentActivity() { super.onPause() nfcAdapter?.disableForegroundDispatch(this) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/alarm/AlarmReceiver.kt b/app/src/main/java/com/mapgie/dash/alarm/AlarmReceiver.kt index 6a8d816..32fab99 100644 --- a/app/src/main/java/com/mapgie/dash/alarm/AlarmReceiver.kt +++ b/app/src/main/java/com/mapgie/dash/alarm/AlarmReceiver.kt @@ -34,4 +34,4 @@ class AlarmReceiver : BroadcastReceiver() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/alarm/AlarmScheduler.kt b/app/src/main/java/com/mapgie/dash/alarm/AlarmScheduler.kt index c9f91ef..b1c8456 100644 --- a/app/src/main/java/com/mapgie/dash/alarm/AlarmScheduler.kt +++ b/app/src/main/java/com/mapgie/dash/alarm/AlarmScheduler.kt @@ -55,4 +55,4 @@ class AlarmScheduler @Inject constructor( PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/alarm/BootReceiver.kt b/app/src/main/java/com/mapgie/dash/alarm/BootReceiver.kt index fc721c4..5ffd250 100644 --- a/app/src/main/java/com/mapgie/dash/alarm/BootReceiver.kt +++ b/app/src/main/java/com/mapgie/dash/alarm/BootReceiver.kt @@ -17,4 +17,4 @@ class BootReceiver : BroadcastReceiver() { WorkManager.getInstance(context) .enqueue(OneTimeWorkRequestBuilder().build()) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/alarm/BootWorker.kt b/app/src/main/java/com/mapgie/dash/alarm/BootWorker.kt index 110bd91..4c8a579 100644 --- a/app/src/main/java/com/mapgie/dash/alarm/BootWorker.kt +++ b/app/src/main/java/com/mapgie/dash/alarm/BootWorker.kt @@ -25,4 +25,4 @@ class BootWorker @AssistedInject constructor( } Result.success() }.getOrElse { Result.retry() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/alarm/DailyStaleChoreWorker.kt b/app/src/main/java/com/mapgie/dash/alarm/DailyStaleChoreWorker.kt index e43e678..97197ef 100644 --- a/app/src/main/java/com/mapgie/dash/alarm/DailyStaleChoreWorker.kt +++ b/app/src/main/java/com/mapgie/dash/alarm/DailyStaleChoreWorker.kt @@ -25,4 +25,4 @@ class DailyStaleChoreWorker @AssistedInject constructor( NotificationHelper.showStaleChoresSummary(applicationContext, staleLabels) Result.success() }.getOrElse { Result.retry() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/data/repository/ChoreRepository.kt b/app/src/main/java/com/mapgie/dash/data/repository/ChoreRepository.kt index 5d8665d..0950611 100644 --- a/app/src/main/java/com/mapgie/dash/data/repository/ChoreRepository.kt +++ b/app/src/main/java/com/mapgie/dash/data/repository/ChoreRepository.kt @@ -106,7 +106,6 @@ class ChoreRepository @Inject constructor( } } -// Used only internally for the owners query; owners table has just a handle column. @kotlinx.serialization.Serializable private data class OwnerDto(@kotlinx.serialization.SerialName("handle") val handle: String) diff --git a/app/src/main/java/com/mapgie/dash/data/repository/TaskRepository.kt b/app/src/main/java/com/mapgie/dash/data/repository/TaskRepository.kt index c4198b4..92ddcb3 100644 --- a/app/src/main/java/com/mapgie/dash/data/repository/TaskRepository.kt +++ b/app/src/main/java/com/mapgie/dash/data/repository/TaskRepository.kt @@ -62,18 +62,15 @@ class TaskRepository @Inject constructor( client.from("todos").delete { filter { eq("id", taskId) } } } - /** Returns tasks with a future reminder_at that have not yet been reminded. */ suspend fun pendingReminders(): List { val client = requireClient() return client.from("todos") .select { - filter { - eq("reminded", false) - isNull("completed_at") - } + filter { eq("reminded", false) } } .decodeList() .filter { dto -> + if (dto.completedAt != null) return@filter false val reminderAt = dto.reminderAt?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return@filter false diff --git a/app/src/main/java/com/mapgie/dash/di/AppModule.kt b/app/src/main/java/com/mapgie/dash/di/AppModule.kt index 1f59e6a..dc96e65 100644 --- a/app/src/main/java/com/mapgie/dash/di/AppModule.kt +++ b/app/src/main/java/com/mapgie/dash/di/AppModule.kt @@ -1,12 +1,7 @@ package com.mapgie.dash.di - import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent - -// All repositories and infrastructure classes use @Inject constructor and -// @Singleton, so Hilt auto-provides them without explicit @Provides methods. -// This module exists as an extension point for future additions. @Module @InstallIn(SingletonComponent::class) -object AppModule +object AppModule \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/di/SupabaseModule.kt b/app/src/main/java/com/mapgie/dash/di/SupabaseModule.kt index 9f82262..9597e40 100644 --- a/app/src/main/java/com/mapgie/dash/di/SupabaseModule.kt +++ b/app/src/main/java/com/mapgie/dash/di/SupabaseModule.kt @@ -1,11 +1,7 @@ package com.mapgie.dash.di - import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent - -// SupabaseClientProvider is @Singleton + @Inject; no manual @Provides needed. -// This module exists as an extension point for mock clients in tests. @Module @InstallIn(SingletonComponent::class) -object SupabaseModule +object SupabaseModule \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/nfc/NfcHandler.kt b/app/src/main/java/com/mapgie/dash/nfc/NfcHandler.kt index 766d227..33fa658 100644 --- a/app/src/main/java/com/mapgie/dash/nfc/NfcHandler.kt +++ b/app/src/main/java/com/mapgie/dash/nfc/NfcHandler.kt @@ -55,4 +55,4 @@ object NfcHandler { } else -> null } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/notification/NotificationHelper.kt b/app/src/main/java/com/mapgie/dash/notification/NotificationHelper.kt index ea9b421..102a624 100644 --- a/app/src/main/java/com/mapgie/dash/notification/NotificationHelper.kt +++ b/app/src/main/java/com/mapgie/dash/notification/NotificationHelper.kt @@ -92,4 +92,4 @@ object NotificationHelper { NotificationManagerCompat.from(context).notify(1, notification) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/components/ChoreCard.kt b/app/src/main/java/com/mapgie/dash/ui/components/ChoreCard.kt index 088b83c..7b07ec9 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/ChoreCard.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/ChoreCard.kt @@ -114,4 +114,4 @@ private fun lastScannedText(lastScanned: Instant?): String { days == 1L -> "Yesterday" else -> "${days}d ago" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/components/EditChoreSheet.kt b/app/src/main/java/com/mapgie/dash/ui/components/EditChoreSheet.kt index a9ce87b..baf05fc 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/EditChoreSheet.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/EditChoreSheet.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.SecureFlagPolicy import com.mapgie.dash.data.model.Chore import kotlinx.coroutines.launch @@ -34,7 +33,6 @@ fun EditChoreSheet( onDismissRequest = { sheetScope.launch { sheetState.show() } }, sheetState = sheetState, properties = ModalBottomSheetProperties( - securePolicy = SecureFlagPolicy.Inherit, isFocusable = true, shouldDismissOnBackPress = false ) @@ -72,7 +70,7 @@ fun EditChoreSheet( label = { Text("Owner") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = ownerExpanded) }, modifier = Modifier - .menuAnchor() + .menuAnchor(MenuAnchorType.PrimaryNotEditable) .fillMaxWidth() ) ExposedDropdownMenu( @@ -162,4 +160,4 @@ fun EditChoreSheet( } ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/components/EditTaskSheet.kt b/app/src/main/java/com/mapgie/dash/ui/components/EditTaskSheet.kt index 842ec15..9446c72 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/EditTaskSheet.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/EditTaskSheet.kt @@ -21,11 +21,11 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.SecureFlagPolicy import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -150,7 +150,6 @@ fun EditTaskSheet( onDismissRequest = { sheetScope.launch { sheetState.show() } }, sheetState = sheetState, properties = ModalBottomSheetProperties( - securePolicy = SecureFlagPolicy.Inherit, isFocusable = true, shouldDismissOnBackPress = false ) @@ -189,7 +188,7 @@ fun EditTaskSheet( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) }, modifier = Modifier .fillMaxWidth() - .menuAnchor() + .menuAnchor(MenuAnchorType.PrimaryEditable) ) val filtered = categories.filter { it.contains(category, ignoreCase = true) } if (filtered.isNotEmpty()) { @@ -298,7 +297,7 @@ fun EditTaskSheet( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = ownerExpanded) }, modifier = Modifier .fillMaxWidth() - .menuAnchor() + .menuAnchor(MenuAnchorType.PrimaryEditable) ) if (owners.isNotEmpty()) { ExposedDropdownMenu( @@ -446,4 +445,4 @@ fun EditTaskSheet( } ) { DatePicker(state = reminderPickerState) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/components/LogBottomSheet.kt b/app/src/main/java/com/mapgie/dash/ui/components/LogBottomSheet.kt index 824c11d..ee723c3 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/LogBottomSheet.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/LogBottomSheet.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.SecureFlagPolicy import com.mapgie.dash.data.model.Chore import kotlinx.coroutines.launch import java.time.Instant @@ -29,13 +28,10 @@ fun LogBottomSheet( var selectedMinute by remember { mutableIntStateOf(LocalTime.now().minute) } var showDatePicker by remember { mutableStateOf(false) } - // Bounce sheet back on backdrop tap or swipe — prevents the "stuck invisible overlay" - // issue documented in GoFlo LESSONS.md. User can only exit via explicit Cancel/Confirm. ModalBottomSheet( onDismissRequest = { sheetScope.launch { sheetState.show() } }, sheetState = sheetState, properties = ModalBottomSheetProperties( - securePolicy = SecureFlagPolicy.Inherit, isFocusable = true, shouldDismissOnBackPress = false ) diff --git a/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt b/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt index e1b061a..3e96de2 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt @@ -172,4 +172,4 @@ private fun DueBadge(task: TaskDto) { style = MaterialTheme.typography.labelSmall, color = color ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt b/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt index f96fb2b..d6925ac 100644 --- a/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt +++ b/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt @@ -103,4 +103,4 @@ fun DashNavGraph( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListScreen.kt b/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListScreen.kt index ffd977e..a68ec4d 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListScreen.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListScreen.kt @@ -365,4 +365,4 @@ private fun EmptyState(message: String) { modifier = Modifier.padding(32.dp) ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListViewModel.kt b/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListViewModel.kt index 48cc4fd..72849f0 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListViewModel.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/chores/ChoreListViewModel.kt @@ -158,4 +158,4 @@ class ChoreListViewModel @Inject constructor( fun setOwnerFilter(ownerFilter: OwnerFilter) { _uiState.update { it.copy(ownerFilter = ownerFilter) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/licenses/LicensesScreen.kt b/app/src/main/java/com/mapgie/dash/ui/screens/licenses/LicensesScreen.kt index 568ebba..6e86668 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/licenses/LicensesScreen.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/licenses/LicensesScreen.kt @@ -76,4 +76,4 @@ fun LicensesScreen(onBack: () -> Unit) { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt index 2857d25..881214c 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -61,14 +62,12 @@ fun SettingsScreen( val saveError by viewModel.saveError.collectAsState() val snackbarHost = remember { SnackbarHostState() } - // Seed local fields once settings load var url by rememberSaveable(settings?.supabaseUrl) { mutableStateOf(settings?.supabaseUrl ?: "") } var key by rememberSaveable(settings?.supabaseKey) { mutableStateOf(settings?.supabaseKey ?: "") } var owner by rememberSaveable(settings?.ownerHandle) { mutableStateOf(settings?.ownerHandle ?: "") } var keyVisible by remember { mutableStateOf(false) } var ownerExpanded by remember { mutableStateOf(false) } - // Load owners when screen is first shown (after potential existing credentials) LaunchedEffect(Unit) { viewModel.loadOwners() } LaunchedEffect(saveError) { @@ -138,7 +137,7 @@ fun SettingsScreen( }, modifier = Modifier .fillMaxWidth() - .menuAnchor() + .menuAnchor(MenuAnchorType.PrimaryEditable) ) if (owners.isNotEmpty()) { ExposedDropdownMenu( @@ -212,4 +211,4 @@ private val ThemeMode.label: String ThemeMode.SYSTEM -> "System" ThemeMode.LIGHT -> "Light" ThemeMode.DARK -> "Dark" - } + } \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsViewModel.kt index 5640877..a11f12f 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsViewModel.kt @@ -47,5 +47,5 @@ class SettingsViewModel @Inject constructor( } } - fun clearSaveError() = _saveError.value = null + fun clearSaveError() { _saveError.value = null } } diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt index c4e1ff9..693111f 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt @@ -275,4 +275,4 @@ private fun TaskSort.next(): TaskSort = when (this) { TaskSort.PRIORITY -> TaskSort.DUE TaskSort.DUE -> TaskSort.CREATED TaskSort.CREATED -> TaskSort.PRIORITY -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt index 65237f3..985d092 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt @@ -192,4 +192,4 @@ class TaskListViewModel @Inject constructor( fun setGroupBy(enabled: Boolean) = _uiState.update { it.copy(groupByCategory = enabled) } fun setOwnerFilter(f: OwnerFilter) = _uiState.update { it.copy(ownerFilter = f) } fun clearError() = _uiState.update { it.copy(error = null) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/theme/Color.kt b/app/src/main/java/com/mapgie/dash/ui/theme/Color.kt index c29118d..7ca7a19 100644 --- a/app/src/main/java/com/mapgie/dash/ui/theme/Color.kt +++ b/app/src/main/java/com/mapgie/dash/ui/theme/Color.kt @@ -67,4 +67,4 @@ val md_theme_dark_scrim = Color(0xFF000000) // Semantic status colours referenced by card left-bars val StatusStale = Color(0xFFBA1A1A) val StatusAging = Color(0xFFF29900) -val StatusFresh = Color(0xFF4A7C59) +val StatusFresh = Color(0xFF4A7C59) \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/theme/Theme.kt b/app/src/main/java/com/mapgie/dash/ui/theme/Theme.kt index 70b2747..f9bb34e 100644 --- a/app/src/main/java/com/mapgie/dash/ui/theme/Theme.kt +++ b/app/src/main/java/com/mapgie/dash/ui/theme/Theme.kt @@ -95,4 +95,4 @@ fun DashTheme( typography = DashTypography, content = content ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/mapgie/dash/ui/theme/Type.kt b/app/src/main/java/com/mapgie/dash/ui/theme/Type.kt index 52befef..a6b6714 100644 --- a/app/src/main/java/com/mapgie/dash/ui/theme/Type.kt +++ b/app/src/main/java/com/mapgie/dash/ui/theme/Type.kt @@ -57,4 +57,4 @@ val DashTypography = Typography( lineHeight = 16.sp, letterSpacing = 0.5.sp ) -) +) \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index d378acd..bbd3e02 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index d378acd..bbd3e02 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 79f5cc8..659588b 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,4 +1,4 @@