Output:
Chunk ID: 9856aa
Wall time: 0.2100 seconds
Process exited with code 0
Original token count: 4303
Output:
build.gradle.kts
settings.gradle.kts
TESTING_SETUP.md
GEMINI.md
CLAUDE.md
gradlew.bat
gradlew
gradle.properties
sample-vault/generate.py
sample-vault/README.md
gradle/libs.versions.toml
gradle/wrapper/gradle-wrapper.properties
gradle/wrapper/gradle-wrapper.jar
docs/plan-integration-tests-restructure.md
docs/specs.md
docs/adr.md
docs/adr-011-navigation-transitions.md
docs/sentry-setup.md
docs/privacy-policy.md
docs/plan-smoke-tests.md
docs/plan-paywall.md
app/keystore.properties.template
app/build.gradle.kts
app/proguard-rules.pro
app/lint-baseline.xml
app/detekt-baseline.xml
app/coverage-baselines/gmd_smoke.ec
config/detekt/detekt.yml
app/src/androidTest/assets/test_receipt.jpg
app/src/androidTest/java/com/yorvana/SmokeTest.kt
app/src/testDebug/java/com/yorvana/data/billing/BillingManagerImplTest.kt
app/src/test/java/com/yorvana/util/VaultPathHelperTest.kt
app/src/test/java/com/yorvana/util/UuidGeneratorTest.kt
app/src/test/java/com/yorvana/util/SentryWrapperTest.kt
app/src/test/java/com/yorvana/util/FileProviderHelperTest.kt
app/src/test/java/com/yorvana/util/DateFormatterTest.kt
app/src/test/java/com/yorvana/util/CrashReporterTest.kt
app/src/main/java/com/yorvana/util/VaultPathHelper.kt
app/src/main/java/com/yorvana/util/UuidGenerator.kt
app/src/main/java/com/yorvana/util/SentryWrapper.kt
app/src/main/java/com/yorvana/util/SentryInitParams.kt
app/src/main/java/com/yorvana/util/FileProviderHelper.kt
app/src/main/java/com/yorvana/util/DateFormatter.kt
app/src/main/java/com/yorvana/util/CrashReporter.kt
app/src/main/java/com/yorvana/ui/vehicles/VehiclePreview.kt
app/src/main/java/com/yorvana/ui/vehicles/VehicleListViewModel.kt
app/src/main/java/com/yorvana/ui/vehicles/VehicleListScreen.kt
app/src/main/java/com/yorvana/ui/vehicles/AddEditVehicleViewModel.kt
app/src/main/java/com/yorvana/ui/vehicles/AddEditVehicleScreen.kt
app/src/main/java/com/yorvana/ui/util/ViewModelFactory.kt
app/src/main/java/com/yorvana/ui/util/ViewModelDependencies.kt
app/src/main/java/com/yorvana/ui/util/GatedViewModel.kt
app/src/main/java/com/yorvana/ui/util/ContextExtensions.kt
app/src/main/java/com/yorvana/ui/util/BillingUIUtils.kt
app/src/test/java/com/yorvana/ui/vehicles/snapshots/VehicleListScreen_list_dark.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/VehicleListScreen_list_light.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/VehicleListScreen_readOnly_light.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/VehicleListScreen_empty_dark.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/VehicleListScreen_readOnly_dark.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/VehicleListScreen_empty_light.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/AddEditVehicleScreen_add_dark.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/AddEditVehicleScreen_add_light.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/AddEditVehicleScreen_edit_dark.png
app/src/test/java/com/yorvana/ui/vehicles/snapshots/AddEditVehicleScreen_edit_light.png
app/src/test/java/com/yorvana/ui/vehicles/VehicleListViewModelTest.kt
app/src/test/java/com/yorvana/ui/vehicles/VehicleListScreenshotTest.kt
app/src/test/java/com/yorvana/ui/vehicles/VehicleListScreenIT.kt
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleViewModelTest.kt
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleViewModelIT.kt
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleScreenshotTest.kt
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleScreenTest.kt
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleScreenIT.kt
app/src/test/java/com/yorvana/ui/util/ViewModelFactoryTest.kt
app/src/test/java/com/yorvana/ui/util/ViewModelDependenciesExt.kt
app/src/test/java/com/yorvana/ui/util/ContextExtensionsTest.kt
app/src/test/java/com/yorvana/ui/util/BaseScreenshotTest.kt
app/src/main/java/com/yorvana/ui/theme/Type.kt
app/src/main/java/com/yorvana/ui/theme/Theme.kt
app/src/main/java/com/yorvana/ui/theme/Color.kt
app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt
app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt
app/src/main/java/com/yorvana/ui/settings/CategoriesViewModel.kt
app/src/main/java/com/yorvana/ui/settings/CategoriesScreen.kt
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_existing_vault_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_conflict_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/CategoriesScreen_add_dialog.png
app/src/test/java/com/yorvana/ui/settings/snapshots/CategoriesScreen_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/CategoriesScreen_dark.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_crash_reporting_on_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_privacy_section_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_premium_dark.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_privacy_dialog.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_debug_crash_section_dark.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_dark.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_premium_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_validating_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_light.png
app/src/test/java/com/yorvana/ui/settings/snapshots/SettingsScreen_debug_crash_section_light.png
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelIT.kt
app/src/test/java/com/yorvana/ui/settings/SettingsScreenshotTest.kt
app/src/test/java/com/yorvana/ui/settings/SettingsScreenIT.kt
app/src/test/java/com/yorvana/ui/settings/CategoriesViewModelTest.kt
app/src/test/java/com/yorvana/ui/settings/CategoriesScreenshotTest.kt
app/src/test/java/com/yorvana/ui/settings/CategoriesScreenTest.kt
app/src/test/java/com/yorvana/ui/settings/CategoriesScreenIT.kt
app/src/main/java/com/yorvana/ui/reportbug/ReportBugViewModel.kt
app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt
app/src/main/java/com/yorvana/ui/reportbug/DeviceInfoDialog.kt
app/src/main/java/com/yorvana/domain/model/VehicleWithMeta.kt
app/src/main/java/com/yorvana/domain/model/RecordWithCategory.kt
app/src/main/java/com/yorvana/domain/AppGate.kt
app/src/main/java/com/yorvana/ui/records/VehicleDetailViewModel.kt
app/src/main/java/com/yorvana/ui/records/VehicleDetailScreen.kt
app/src/main/java/com/yorvana/ui/records/RecordDetailViewModel.kt
app/src/main/java/com/yorvana/ui/records/RecordDetailScreen.kt
app/src/main/java/com/yorvana/ui/records/PerformerUtil.kt
app/src/main/java/com/yorvana/ui/records/ImageViewerScreen.kt
app/src/main/java/com/yorvana/ui/records/AddEditRecordViewModel.kt
app/src/main/java/com/yorvana/ui/records/AddEditRecordScreen.kt
app/src/test/java/com/yorvana/ui/reportbug/snapshots/ReportBugScreen_disclosure_light.png
app/src/test/java/com/yorvana/ui/reportbug/snapshots/ReportBugScreen_device_info_dialog_light.png
app/src/test/java/com/yorvana/ui/reportbug/snapshots/ReportBugScreen_filled_dark.png
app/src/test/java/com/yorvana/ui/reportbug/snapshots/ReportBugScreen_empty_light.png
app/src/test/java/com/yorvana/ui/reportbug/snapshots/ReportBugScreen_submitting_light.png
app/src/test/java/com/yorvana/ui/reportbug/snapshots/ReportBugScreen_invalid_email_dark.png
app/src/test/java/com/yorvana/ui/reportbug/snapshots/ReportBugScreen_invalid_email_light.png
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt
app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenshotTest.kt
app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenTest.kt
app/src/main/java/com/yorvana/data/storage/VaultStorageImpl.kt
app/src/main/java/com/yorvana/data/storage/VaultStorage.kt
app/src/main/java/com/yorvana/data/storage/MigrationHelper.kt
app/src/main/java/com/yorvana/ui/navigation/Screen.kt
app/src/main/java/com/yorvana/ui/navigation/AppNavGraph.kt
app/src/test/java/com/yorvana/YorvanaApplicationTest.kt
app/src/test/java/com/yorvana/TestYorvanaApplication.kt
app/src/test/java/com/yorvana/MainActivityTest.kt
app/src/test/java/com/yorvana/LocalOnly.kt
app/src/main/java/com/yorvana/ui/components/UpgradeDialog.kt
app/src/main/java/com/yorvana/ui/components/TopBarBackButton.kt
app/src/main/java/com/yorvana/ui/components/ShimmerBrush.kt
app/src/main/java/com/yorvana/ui/components/RequiredLabel.kt
app/src/main/java/com/yorvana/ui/components/PrivacyPolicyDialog.kt
app/src/main/java/com/yorvana/ui/components/PrivacyPolicyContent.kt
app/src/main/java/com/yorvana/ui/components/PaywallStatusCard.kt
app/src/main/java/com/yorvana/ui/components/PaywallScaffold.kt
app/src/main/java/com/yorvana/ui/components/PaywallInfoSheet.kt
app/src/main/java/com/yorvana/ui/components/MakePickerDialog.kt
app/src/main/java/com/yorvana/ui/components/ExpandablePrivacyDisclosure.kt
app/src/main/java/com/yorvana/ui/components/ErrorListItem.kt
app/src/main/java/com/yorvana/ui/components/EmptyStateView.kt
app/src/main/java/com/yorvana/ui/components/CurrencyPickerDialog.kt
app/src/main/java/com/yorvana/data/repository/VehicleRepositoryImpl.kt
app/src/main/java/com/yorvana/data/repository/VehicleRepository.kt
app/src/main/java/com/yorvana/data/repository/RecordRepositoryImpl.kt
app/src/main/java/com/yorvana/data/repository/RecordRepository.kt
app/src/main/java/com/yorvana/data/repository/CategoryRepositoryImpl.kt
app/src/main/java/com/yorvana/data/repository/CategoryRepository.kt
app/src/main/java/com/yorvana/ui/components/ConfirmDeleteDialog.kt
app/src/main/java/com/yorvana/ui/components/ChromeLikeMenu.kt
app/src/main/java/com/yorvana/ui/components/AttachmentThumbnail.kt
app/src/main/java/com/yorvana/ui/components/AttachmentChip.kt
app/src/main/java/com/yorvana/ui/TestTags.kt
app/src/test/java/com/yorvana/domain/AppGateTest.kt
app/src/test/java/com/yorvana/ui/records/RecordDetailScreenIT.kt
app/src/test/java/com/yorvana/ui/records/ImageViewerScreenshotTest.kt
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenshotTest.kt
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenTest.kt
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenIT.kt
app/src/test/java/com/yorvana/ui/records/snapshots/VehicleDetailScreen_light.png
app/src/test/java/com/yorvana/ui/records/snapshots/VehicleDetailScreen_menu_dark.png
app/src/test/java/com/yorvana/ui/records/snapshots/VehicleDetailScreen_menu_light.png
app/src/test/java/com/yorvana/ui/records/snapshots/VehicleDetailScreen_dark.png
app/src/test/java/com/yorvana/ui/records/snapshots/RecordDetailScreen_dark.png
app/src/test/java/com/yorvana/ui/records/snapshots/RecordDetailScreen_light.png
app/src/test/java/com/yorvana/ui/records/snapshots/ImageViewerScreen.png
app/src/test/java/com/yorvana/ui/records/snapshots/AddEditRecordScreen_add_light.png
app/src/test/java/com/yorvana/ui/records/snapshots/AddEditRecordScreen_custom_task_dialog.png
app/src/test/java/com/yorvana/ui/records/snapshots/AddEditRecordScreen_add_dark.png
app/src/test/java/com/yorvana/ui/records/snapshots/AddEditRecordScreen_edit_dark.png
app/src/test/java/com/yorvana/ui/records/snapshots/AddEditRecordScreen_edit_light.png
app/src/test/java/com/yorvana/ui/records/VehicleDetailViewModelTest.kt
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenshotTest.kt
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenTest.kt
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenIT.kt
app/src/test/java/com/yorvana/ui/records/RecordDetailViewModelTest.kt
app/src/test/java/com/yorvana/ui/records/RecordDetailViewModelIT.kt
app/src/test/java/com/yorvana/ui/records/RecordDetailScreenshotTest.kt
app/src/main/java/com/yorvana/YorvanaApplication.kt
app/src/main/java/com/yorvana/MainActivity.kt
app/src/main/AndroidManifest.xml
app/src/main/java/com/yorvana/data/preferences/DebugBillingOverrideMode.kt
app/src/main/java/com/yorvana/data/preferences/AppPreferencesImpl.kt
app/src/main/java/com/yorvana/data/preferences/AppPreferences.kt
app/src/main/java/com/yorvana/data/model/WorkItem.kt
app/src/main/java/com/yorvana/data/model/Vehicle.kt
app/src/main/java/com/yorvana/data/model/ServiceRecord.kt
app/src/main/java/com/yorvana/data/model/Category.kt
app/src/main/java/com/yorvana/data/model/AttachmentMeta.kt
app/src/test/java/com/yorvana/data/storage/VaultStorageImplTest.kt
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt
app/src/test/java/com/yorvana/data/storage/MigrationHelperTest.kt
app/src/main/res/values-night/themes.xml
app/src/main/java/com/yorvana/data/billing/DebugBillingOverride.kt
app/src/main/java/com/yorvana/data/billing/DebugBillingActions.kt
app/src/main/java/com/yorvana/data/billing/BillingResults.kt
app/src/main/java/com/yorvana/data/billing/BillingManagerImpl.kt
app/src/main/java/com/yorvana/data/billing/BillingManager.kt
app/src/main/java/com/yorvana/data/billing/BillingClientProvider.kt
app/src/test/java/com/yorvana/ui/navigation/snapshots/SetupScreen_step1_folder.png
app/src/test/java/com/yorvana/ui/navigation/snapshots/SetupScreen_step2_sentry_privacy_dialog.png
app/src/test/java/com/yorvana/ui/navigation/snapshots/SetupScreen_step2_with_topbar.png
app/src/test/java/com/yorvana/ui/navigation/snapshots/SetupScreen_step2_sentry_off.png
app/src/test/java/com/yorvana/ui/navigation/snapshots/SetupScreen_step2_sentry_on.png
app/src/test/java/com/yorvana/ui/navigation/SetupScreenshotTest.kt
app/src/test/java/com/yorvana/ui/navigation/SetupScreenTest.kt
app/src/main/res/drawable/ic_launcher_monochrome.xml
app/src/main/res/drawable/ic_launcher_foreground.xml
app/src/main/res/drawable/ic_launcher_background.xml
app/src/test/java/com/yorvana/data/repository/WorkflowIntegrationIT.kt
app/src/test/java/com/yorvana/data/repository/VehicleRepositoryImplTest.kt
app/src/test/java/com/yorvana/data/repository/RecordRepositoryImplTest.kt
app/src/test/java/com/yorvana/data/repository/DataIntegrationIT.kt
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt
app/src/test/java/com/yorvana/data/billing/FakeBillingManager.kt
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
app/src/test/java/com/yorvana/data/preferences/AppPreferencesImplTest.kt
app/src/test/java/com/yorvana/ui/components/PaywallStatusCardScreenshotTest.kt
app/src/test/java/com/yorvana/ui/components/PaywallScaffoldScreenshotTest.kt
app/src/test/java/com/yorvana/ui/components/PaywallInfoSheetScreenshotTest.kt
app/src/test/java/com/yorvana/ui/components/ComponentScreenshotTest.kt
app/src/test/java/com/yorvana/ui/components/UpgradeDialogScreenshotTest.kt
app/src/test/java/com/yorvana/ui/components/RequiredLabelTest.kt
app/src/test/java/com/yorvana/ui/components/UpgradeDialogTest.kt
app/src/main/res/values/themes.xml
app/src/main/res/values/strings.xml
app/src/main/res/values/colors.xml
app/src/main/res/xml/file_provider_paths.xml
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallInfoSheet_readOnly_dark.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallInfoSheet_readOnly_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallInfoSheet_pending_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallStatusCard_readOnly_dark.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallInfoSheet_pending_dark.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallStatusCard_readOnly_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallStatusCard_pending_dark.png
app/src/test/java/com/yorvana/ui/components/snapshots/MakePickerDialog.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallStatusCard_pending_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/UpgradeDialog_dark.png
app/src/test/java/com/yorvana/ui/components/snapshots/AttachmentChip_readOnly.png
app/src/test/java/com/yorvana/ui/components/snapshots/UpgradeDialog_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/CurrencyPickerDialog.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallScaffold_noSheet_dark.png
app/src/test/java/com/yorvana/ui/components/snapshots/PaywallScaffold_noSheet_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/EmptyStateView_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/RequiredLabel.png
app/src/test/java/com/yorvana/ui/components/snapshots/ErrorListItem_light.png
app/src/test/java/com/yorvana/ui/components/snapshots/AttachmentThumbnail_noUri.png
app/src/test/java/com/yorvana/ui/components/snapshots/CustomCurrencyDialog.png
app/src/test/java/com/yorvana/ui/components/snapshots/ConfirmDeleteDialog.png
app/src/test/java/com/yorvana/ui/components/snapshots/ChromeLikeMenu.png
app/src/test/java/com/yorvana/ui/components/snapshots/AttachmentChip_deletable.png
app/src/test/java/com/yorvana/ui/components/snapshots/EmptyStateView_dark.png
app/src/test/java/com/yorvana/ui/components/snapshots/RequiredLabel_default.png


I found this is an Android Compose app. The Settings screen already has an About section with “Report a bug or suggest a feature,” and there is existing report-bug navigation, so I’m checking whether “Contact” should reuse that flow or be a separate mail action.


sed -n '1,260p' app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt


sed -n '1,220p' app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt


sed -n '1,240p' app/src/main/java/com/yorvana/ui/navigation/AppNavGraph.kt


sed -n '1,80p' app/src/main/java/com/yorvana/ui/navigation/Screen.kt


Output:
Chunk ID: 8379c8
Wall time: 0.3662 seconds
Process exited with code 0
Original token count: 2792
Output:
package com.yorvana.ui.settings

import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.DocumentsContract
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.automirrored.outlined.ArrowForwardIos
import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Category
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import com.yorvana.BuildConfig
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.data.model.OdometerUnit
import com.yorvana.data.preferences.DebugBillingOverrideMode
import com.yorvana.ui.TestTags
import com.yorvana.ui.components.CurrencyPickerDialog
import com.yorvana.ui.components.CustomCurrencyDialog
import com.yorvana.ui.components.DismissablePendingDialog
import com.yorvana.ui.components.PaywallScaffold
import com.yorvana.ui.components.PrivacyPolicyDialog
import com.yorvana.ui.components.TopBarBackButton
import com.yorvana.ui.navigation.Categories
import com.yorvana.ui.navigation.ReportBug
import com.yorvana.ui.theme.appTopAppBarColors
import com.yorvana.ui.theme.paywallContainerColor
import com.yorvana.ui.theme.paywallContentColor
import com.yorvana.ui.util.ViewModelFactory
import com.yorvana.ui.util.rememberUpgradeLauncher
import com.yorvana.util.VaultPathHelper

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
    backStack: NavBackStack<NavKey>,
    viewModel: SettingsViewModel =
        viewModel(
            factory = ViewModelFactory(LocalContext.current.applicationContext as YorvanaApplication),
        ),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    val context = LocalContext.current
    val snackbarHostState = remember { SnackbarHostState() }
    val isDark = isSystemInDarkTheme()
    val upgradeLauncher = rememberUpgradeLauncher(snackbarHostState)
    val fileManagerError = stringResource(R.string.settings_file_manager_error)

    var showOdometerDialog by remember { mutableStateOf(false) }
    var showCurrencyDialog by remember { mutableStateOf(false) }
    var showCustomCurrencyDialog by remember { mutableStateOf(false) }
    var showCrashConfirmation by remember { mutableStateOf(false) }
    var showPrivacyDialog by rememberSaveable { mutableStateOf(false) }

    if (showPrivacyDialog) {
        PrivacyPolicyDialog(onDismissRequest = { showPrivacyDialog = false })
    }

    val folderPicker =
        rememberLauncherForActivityResult(
            ActivityResultContracts.OpenDocumentTree(),
        ) { uri ->
            if (uri == null) return@rememberLauncherForActivityResult
            context.contentResolver.takePersistableUriPermission(
                uri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
            )
            viewModel.onEvent(SettingsEvent.VaultFolderPicked(uri))
        }

    LaunchedEffect(Unit) {
        viewModel.effects.collect { effect ->
            when (effect) {
                SettingsEffect.NavigateToCategories ->
                    backStack.add(Categories)
                SettingsEffect.NavigateToReportBug ->
                    backStack.add(ReportBug)
                is SettingsEffect.ShowSnackbar ->
                    snackbarHostState.showSnackbar(effect.message)
                is SettingsEffect.LaunchFileManager -> {
                    try {
                        val treeUri = android.net.Uri.parse(effect.vaultUriString)
                        val docUri =
                            DocumentsContract.buildDocumentUriUsingTree(
                                treeUri,
                                DocumentsContract.getTreeDocumentId(treeUri),
                            )
                        val intent =
                            Intent(Intent.ACTION_VIEW).apply {
                                setDataAndType(docUri, DocumentsContract.Document.MIME_TYPE_DIR)
                                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                            }
                        context.startActivity(intent)
                    } catch (_: ActivityNotFoundException) {
                        snackbarHostState.showSnackbar(fileManagerError)
                    }
                }
                SettingsEffect.LaunchPurchaseFlow -> {
                    upgradeLauncher()
                }
            }
        }
    }

    if (state.showMoveOrFreshDialog) {
        AlertDialog(
            onDismissRequest = { viewModel.onEvent(SettingsEvent.DismissMoveDialog) },
            title = { Text(stringResource(R.string.settings_vault_move_title)) },
            text = { Text(stringResource(R.string.settings_vault_move_message)) },
            confirmButton = {
                TextButton(onClick = { viewModel.onEvent(SettingsEvent.MoveVault) }) {
                    Text(stringResource(R.string.settings_vault_move_action))
                }
            },
            dismissButton = {
                Row {
                    TextButton(onClick = { viewModel.onEvent(SettingsEvent.StartFresh) }) {
                        Text(stringResource(R.string.settings_vault_fresh_action))
                    }
                    TextButton(onClick = { viewModel.onEvent(SettingsEvent.DismissMoveDialog) }) {
                        Text(stringResource(R.string.action_cancel))
                    }
                }
            },
        )
    }

    if (state.isValidating) {
        AlertDialog(
            onDismissRequest = { },
            confirmButton = { },
            title = {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    CircularProgressIndicator(modifier = Modifier.size(24.dp))
                    Spacer(Modifier.size(16.dp))
                    Text(stringResource(R.string.settings_vault_validating))
                }
            },
        )
    }

    if (state.showUseExistingVaultDialog) {
        AlertDialog(
            onDismissRequest = { viewModel.onEvent(SettingsEvent.DismissUseExistingVaultDialog) },
            title = { Text(stringResource(R.string.settings_vault_existing_title)) },
            text = { Text(stringResource(R.string.settings_vault_existing_message)) },
            confirmButton = {
                TextButton(onClick = { viewModel.onEvent(SettingsEvent.StartFresh) }) {
                    Text(stringResource(R.string.settings_vault_use_existing_action))
                }
            },
            dismissButton = {
                TextButton(onClick = { viewModel.onEvent(SettingsEvent.DismissUseExistingVaultDialog) }) {
                    Text(stringResource(R.string.action_cancel))
                }
            },
        )
    }

    if (state.showConflictDialog) {
        AlertDialog(
            onDismissRequest = { viewModel.onEvent(SettingsEvent.DismissConflictDialog) },
            title = { Text(stringResource(R.string.settings_vault_conflict_title)) },
            text = { Text(stringResource(R.string.settings_vault_conflict_message)) },
            confirmButton = {
                TextButton(onClick = { viewModel.onEvent(SettingsEvent.DismissConflictDialog) }) {
                    Text(stringResource(R.string.action_ok))
                }
            },
        )
    }

    DismissablePendingDialog(visible = state.isPremiumPending)

    if (showOdometerDialog) {
        OdometerUnitDialog(
            current = state.defaultOdometerUnit,
            onSelect = { unit ->
                viewModel.onEvent(SettingsEvent.OdometerUnitChanged(unit))
                showOdometerDialog = false
            },
            onDismiss = { showOdometerDialog = false },
        )
    }

    if (showCurrencyDialog) {
        CurrencyPickerDialog(
            current = state.defaultCurrency,
            onSelect = { currency ->
                viewModel.onEvent(SettingsEvent.CurrencyChanged(currency))
                showCurrencyDialog = false
            },
            onCustom = {
                showCurrencyDialog = false
                showCustomCurrencyDialog = true
            },
            onDismiss = { showCurrencyDialog = false },
        )
    }

    if (showCustomCurrencyDialog) {
        CustomCurrencyDialog(
            onConfirm = { currency ->


Output:
Chunk ID: 2914e4
Wall time: 0.4164 seconds
Process exited with code 0
Original token count: 2289
Output:
package com.yorvana.ui.settings

import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.yorvana.BuildConfig
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.data.billing.DebugBillingActions
import com.yorvana.data.billing.RestoreResult
import com.yorvana.data.model.OdometerUnit
import com.yorvana.data.preferences.DebugBillingOverrideMode
import com.yorvana.data.storage.DestinationNotEmptyException
import com.yorvana.ui.util.GatedViewModel
import com.yorvana.ui.util.ViewModelDependencies
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

data class SettingsState(
    val vaultUriString: String? = null,
    val defaultOdometerUnit: OdometerUnit = OdometerUnit.KM,
    val defaultCurrency: String = "USD",
    val appVersion: String = "",
    val isValidating: Boolean = false,
    val isMigrating: Boolean = false,
    val showMoveOrFreshDialog: Boolean = false,
    val showConflictDialog: Boolean = false,
    val showUseExistingVaultDialog: Boolean = false,
    val pendingNewUri: Uri? = null,
    val isPremium: Boolean = false,
    val isPremiumPending: Boolean = false,
    /** Added to feed Phase 5 UI (disabling buttons/showing banners); logic gating is in VM. */
    val isReadOnly: Boolean = false,
    val isRestoringPurchases: Boolean = false,
    val debugOverrideMode: DebugBillingOverrideMode = DebugBillingOverrideMode.NONE,
    val crashReportingEnabled: Boolean = false,
)

sealed class SettingsEvent {
    data object OpenCategories : SettingsEvent()

    data object OpenReportBug : SettingsEvent()

    data object OpenFileManager : SettingsEvent()

    data class VaultFolderPicked(val uri: Uri) : SettingsEvent()

    data object MoveVault : SettingsEvent()

    data object StartFresh : SettingsEvent()

    data object DismissMoveDialog : SettingsEvent()

    data object DismissConflictDialog : SettingsEvent()

    data object DismissUseExistingVaultDialog : SettingsEvent()

    data class OdometerUnitChanged(val unit: OdometerUnit) : SettingsEvent()

    data class CurrencyChanged(val currency: String) : SettingsEvent()

    data object RequestUpgrade : SettingsEvent()

    data object RestorePurchases : SettingsEvent()

    data object SimulatePurchaseSuccess : SettingsEvent()

    data object ResetBillingState : SettingsEvent()

    data class SetDebugOverride(val mode: DebugBillingOverrideMode) : SettingsEvent()

    data class CrashReportingToggled(val enabled: Boolean) : SettingsEvent()
}

sealed class SettingsEffect {
    data object NavigateToCategories : SettingsEffect()

    data object NavigateToReportBug : SettingsEffect()

    data class LaunchFileManager(val vaultUriString: String) : SettingsEffect()

    data class ShowSnackbar(val message: String) : SettingsEffect()

    data object LaunchPurchaseFlow : SettingsEffect()
}

/**
 * SettingsViewModel manages vault selection, preferences, and billing actions.
 *
 * **State architecture note**: two state holders coexist:
 * - `_state` (`MutableStateFlow<SettingsState>`) holds billing/internal fields (isPremium,
 *   isReadOnly, isRestoringPurchases, debugOverrideMode). Internal logic **must** read these
 *   via `_state.value` — the public `state` flow is `WhileSubscribed`-cached and returns
 *   `initialValue = SettingsState()` when there is no active subscriber.
 * - `state` (public `StateFlow`) is the `combine` of `_state` + preferences. Fields that
 *   originate from preferences (vaultUriString, defaultOdometerUnit, etc.) are **only**
 *   available via `state.value` since they flow exclusively through the combine.
 *
 * This distinction applies to every field added to [SettingsState] in the future.
 */
class SettingsViewModel(
    dependencies: ViewModelDependencies,
) : GatedViewModel(dependencies) {
    private val preferences = dependencies.preferences
    private val vaultStorage = dependencies.vaultStorage
    private val billingManager = dependencies.billingManager
    private val debugBillingOverride = dependencies.debugBillingOverride
    private val _state = MutableStateFlow(SettingsState())
    val state: StateFlow<SettingsState> =
        combine(
            _state,
            preferences.preferences,
        ) { internalState, prefs ->
            internalState.copy(
                vaultUriString = prefs.vaultUriString,
                defaultOdometerUnit = prefs.defaultOdometerUnit,
                defaultCurrency = prefs.defaultCurrency,
                appVersion = getAppVersion(),
                crashReportingEnabled = prefs.crashReportingEnabled,
            )
        }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS),
            initialValue = SettingsState(),
        )

    // BUFFERED so that effects emitted before the UI subscribes (e.g., during tests or when
    // the UI briefly unsubscribes) are not dropped rather than suspending the sender.
    private val _effects = Channel<SettingsEffect>(Channel.BUFFERED)
    val effects = _effects.receiveAsFlow()

    private var currencyChangeJob: Job? = null

    init {
        appGate.isPremium
            .onEach { premium -> _state.update { it.copy(isPremium = premium) } }
            .launchIn(viewModelScope)
        appGate.hasPendingPurchase
            .onEach { pending -> _state.update { it.copy(isPremiumPending = pending) } }
            .launchIn(viewModelScope)
        appGate.isReadOnly
            .onEach { readOnly -> _state.update { it.copy(isReadOnly = readOnly) } }
            .launchIn(viewModelScope)
        debugBillingOverride?.mode
            ?.onEach { mode -> _state.update { it.copy(debugOverrideMode = mode) } }
            ?.launchIn(viewModelScope)
    }

    private fun getAppVersion(): String {
        return try {
            val packageInfo = app.packageManager.getPackageInfo(app.packageName, 0)
            packageInfo.versionName ?: ""
        } catch (e: PackageManager.NameNotFoundException) {
            Log.w("SettingsViewModel", "Could not get app version", e)
            ""
        }
    }

    @Suppress("CyclomaticComplexMethod")
    fun onEvent(event: SettingsEvent) {
        when (event) {
            is SettingsEvent.VaultFolderPicked -> handleVaultPicked(event.uri)
            is SettingsEvent.OdometerUnitChanged ->
                viewModelScope.launch { preferences.setDefaultOdometerUnit(event.unit) }
            is SettingsEvent.CurrencyChanged -> {
                currencyChangeJob?.cancel()
                currencyChangeJob =
                    viewModelScope.launch {
                        delay(CURRENCY_CHANGE_DELAY_MILLIS)
                        preferences.setDefaultCurrency(event.currency)
                    }
            }
            SettingsEvent.MoveVault -> moveVault()
            SettingsEvent.StartFresh -> startFresh()
            SettingsEvent.DismissMoveDialog -> _state.update { it.copy(showMoveOrFreshDialog = false, pendingNewUri = null) }
            SettingsEvent.DismissConflictDialog -> _state.update { it.copy(showConflictDialog = false) }
            SettingsEvent.DismissUseExistingVaultDialog ->
                _state.update {
                    it.copy(
                        showUseExistingVaultDialog = false,
                        pendingNewUri = null,
                    )
                }
            SettingsEvent.OpenCategories -> {
                viewModelScope.launch { _effects.send(SettingsEffect.NavigateToCategories) }
            }
            SettingsEvent.OpenReportBug -> {
                viewModelScope.launch { _effects.send(SettingsEffect.NavigateToReportBug) }
            }
            SettingsEvent.OpenFileManager -> {
                viewModelScope.launch {
                    val uriString = preferences.preferences.first().vaultUriString ?: return@launch
                    _effects.send(SettingsEffect.LaunchFileManager(uriString))
                }
            }
            SettingsEvent.RequestUpgrade ->
                if (!appGate.isPremium.value) {
                    viewModelScope.launch { _effects.send(SettingsEffect.LaunchPurchaseFlow) }
                }
            SettingsEvent.RestorePurchases -> restorePurchases()
            SettingsEvent.SimulatePurchaseSuccess -> handleSimulatePurchaseSuccess()
            SettingsEvent.ResetBillingState -> handleResetBillingState()
            is SettingsEvent.SetDebugOverride -> handleSetDebugOverride(event.mode)
            is SettingsEvent.CrashReportingToggled -> handleCrashReportingToggled(event.enabled)
        }
    }


Output:
Chunk ID: 29315c
Wall time: 0.3961 seconds
Process exited with code 0
Original token count: 2430
Output:
package com.yorvana.ui.navigation

import android.content.Intent
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.ui.TestTags
import com.yorvana.ui.components.PrivacyPolicyDialog
import com.yorvana.ui.components.TopBarBackButton
import com.yorvana.ui.records.AddEditRecordScreen
import com.yorvana.ui.records.ImageViewerScreen
import com.yorvana.ui.records.RecordDetailScreen
import com.yorvana.ui.records.VehicleDetailScreen
import com.yorvana.ui.reportbug.ReportBugScreen
import com.yorvana.ui.settings.CategoriesScreen
import com.yorvana.ui.settings.SettingsScreen
import com.yorvana.ui.theme.appTopAppBarColors
import com.yorvana.ui.vehicles.AddEditVehicleScreen
import com.yorvana.ui.vehicles.VehicleListScreen
import kotlinx.coroutines.launch

val LocalNavBackStack = compositionLocalOf<NavBackStack<NavKey>> { error("No backstack") }

private val MaterialEmphasizedEasing = CubicBezierEasing(0.35f, 0.0f, 0.0f, 1.0f)
private const val STANDARD_DURATION = 450

private fun <T> AnimatedContentTransitionScope<T>.modernForwardSpec(): ContentTransform =
    (
        slideInHorizontally(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing)) { it } +
            fadeIn(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing)) togetherWith
            slideOutHorizontally(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing)) { -it / 4 } +
            fadeOut(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing))
    ).apply {
        targetContentZIndex = 1f
    }

private fun <T> AnimatedContentTransitionScope<T>.modernBackwardSpec(): ContentTransform =
    (
        slideInHorizontally(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing)) { -it / 4 } +
            fadeIn(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing)) togetherWith
            slideOutHorizontally(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing)) { it } +
            fadeOut(tween(STANDARD_DURATION, easing = MaterialEmphasizedEasing))
    ).apply {
        targetContentZIndex = -1f
    }

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppNavGraph(startDestination: NavKey) {
    val backStack = rememberNavBackStack(startDestination)

    CompositionLocalProvider(
        LocalNavBackStack provides backStack,
    ) {
        NavDisplay<NavKey>(
            backStack = backStack,
            modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
            onBack = { backStack.removeLastOrNull() },
            transitionSpec = {
                modernForwardSpec()
            },
            popTransitionSpec = {
                modernBackwardSpec()
            },
            entryProvider =
                entryProvider {
                    entry<Setup> {
                        VanillaEntry {
                            SetupScreen()
                        }
                    }
                    entry<VehicleList> {
                        VanillaEntry {
                            VehicleListScreen(backStack = backStack)
                        }
                    }
                    entry<AddVehicle> {
                        VanillaEntry {
                            AddEditVehicleScreen(vehicleId = null, backStack = backStack)
                        }
                    }
                    entry<EditVehicle> { key ->
                        VanillaEntry {
                            AddEditVehicleScreen(vehicleId = key.vehicleId, backStack = backStack)
                        }
                    }
                    entry<VehicleDetail> { key ->
                        VanillaEntry {
                            VehicleDetailScreen(vehicleId = key.vehicleId, backStack = backStack)
                        }
                    }
                    entry<AddRecord> { key ->
                        VanillaEntry {
                            AddEditRecordScreen(vehicleId = key.vehicleId, recordId = null, backStack = backStack)
                        }
                    }
                    entry<EditRecord> { key ->
                        VanillaEntry {
                            AddEditRecordScreen(vehicleId = key.vehicleId, recordId = key.recordId, backStack = backStack)
                        }
                    }
                    entry<RecordDetail> { key ->
                        VanillaEntry {
                            RecordDetailScreen(vehicleId = key.vehicleId, recordId = key.recordId, backStack = backStack)
                        }
                    }
                    entry<Settings> {
                        VanillaEntry {
                            SettingsScreen(backStack = backStack)
                        }
                    }
                    entry<Categories> {
                        VanillaEntry {
                            CategoriesScreen(backStack = backStack)
                        }
                    }
                    entry<ReportBug> {
                        VanillaEntry {
                            ReportBugScreen(backStack = backStack)
                        }
                    }
                    entry<ImageViewer> { key ->
                        VanillaEntry {
                            ImageViewerScreen(uriString = key.uriString, backStack = backStack)
                        }
                    }
                },
        )
    }
}

@Composable
private fun VanillaEntry(content: @Composable () -> Unit) {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colorScheme.background,
    ) {
        content()
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun SetupStep2Scaffold(
    onBack: () -> Unit,
    content: @Composable () -> Unit,
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(stringResource(R.string.setup_step_2_of_2)) },
                navigationIcon = {
                    TopBarBackButton(onClick = onBack)
                },
                colors = appTopAppBarColors(),
            )
        },
    ) { padding ->
        Box(modifier = Modifier.padding(padding)) {
            content()
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun SetupScreen() {
    val context = LocalContext.current
    val application = context.applicationContext as YorvanaApplication
    val scope = rememberCoroutineScope()

    var step by rememberSaveable { mutableStateOf(1) }
    var selectedUri by rememberSaveable { mutableStateOf<String?>(null) }
    var crashReportingEnabled by rememberSaveable { mutableStateOf(true) }

    BackHandler(enabled = step == 2) { step = 1 }

    val folderPicker =
        rememberLauncherForActivityResult(


Output:
Chunk ID: cddb39
Wall time: 0.3881 seconds
Process exited with code 0
Original token count: 208
Output:
package com.yorvana.ui.navigation

import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable

@Serializable object Setup : NavKey

@Serializable object VehicleList : NavKey

@Serializable object AddVehicle : NavKey

@Serializable data class EditVehicle(val vehicleId: String) : NavKey

@Serializable data class VehicleDetail(val vehicleId: String) : NavKey

@Serializable data class AddRecord(val vehicleId: String) : NavKey

@Serializable data class EditRecord(val vehicleId: String, val recordId: String) : NavKey

@Serializable data class RecordDetail(val vehicleId: String, val recordId: String) : NavKey

@Serializable object Settings : NavKey

@Serializable object Categories : NavKey

@Serializable object ReportBug : NavKey

@Serializable data class ImageViewer(val uriString: String) : NavKey


sed -n '260,620p' app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt


Output:
Chunk ID: ecea91
Wall time: 0.1245 seconds
Process exited with code 0
Original token count: 4687
Output:
            onConfirm = { currency ->
                viewModel.onEvent(SettingsEvent.CurrencyChanged(currency))
                showCustomCurrencyDialog = false
            },
            onDismiss = { showCustomCurrencyDialog = false },
        )
    }

    PaywallScaffold(
        showPaywallSheet = false,
        isPremiumPending = false,
        onDismissPaywallSheet = {},
        snackbarHostState = snackbarHostState,
        topBar = {
            TopAppBar(
                title = { Text(stringResource(R.string.settings_title), modifier = Modifier.testTag(TestTags.SCREEN_TITLE)) },
                navigationIcon = {
                    TopBarBackButton(onClick = { backStack.removeLastOrNull() })
                },
                colors = appTopAppBarColors(),
            )
        },
    ) { padding ->
        Column(
            modifier =
                Modifier
                    .fillMaxSize()
                    .padding(padding)
                    .verticalScroll(rememberScrollState())
                    .padding(horizontal = 16.dp),
        ) {
            // ── Storage ────────────────────────────────────────────────────
            SectionHeader(stringResource(R.string.settings_vault))

            Card(
                modifier = Modifier.fillMaxWidth(),
                shape =
                    if (state.vaultUriString != null) {
                        RoundedCornerShape(
                            topStart = 24.dp,
                            topEnd = 24.dp,
                            bottomStart = 4.dp,
                            bottomEnd = 4.dp,
                        )
                    } else {
                        RoundedCornerShape(24.dp)
                    },
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                ListItem(
                    headlineContent = { Text(stringResource(R.string.settings_vault_location)) },
                    supportingContent = {
                        Text(
                            text =
                                if (state.vaultUriString != null) {
                                    VaultPathHelper.uriToDisplayPath(state.vaultUriString!!)
                                } else {
                                    stringResource(R.string.settings_vault_not_set)
                                },
                            maxLines = 2,
                            overflow = TextOverflow.Ellipsis,
                            color = MaterialTheme.colorScheme.onSurfaceVariant,
                        )
                    },
                    leadingContent = {
                        SettingIcon(
                            icon = Icons.Default.Folder,
                            containerColor = MaterialTheme.colorScheme.primaryContainer,
                            tint = MaterialTheme.colorScheme.onPrimaryContainer,
                        )
                    },
                    trailingContent = {
                        if (state.isMigrating) {
                            CircularProgressIndicator()
                        } else {
                            TextButton(onClick = { folderPicker.launch(null) }) {
                                Text(stringResource(R.string.settings_vault_change))
                            }
                        }
                    },
                )
            }
            if (state.vaultUriString != null) {
                Spacer(Modifier.height(2.dp))
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 24.dp, bottomEnd = 24.dp),
                    colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
                ) {
                    ListItem(
                        headlineContent = { Text(stringResource(R.string.settings_explore_data)) },
                        supportingContent = { Text(stringResource(R.string.settings_explore_data_subtitle)) },
                        leadingContent = {
                            SettingIcon(
                                icon = Icons.AutoMirrored.Filled.OpenInNew,
                                containerColor = MaterialTheme.colorScheme.primaryContainer,
                                tint = MaterialTheme.colorScheme.onPrimaryContainer,
                            )
                        },
                        modifier = Modifier.clickable { viewModel.onEvent(SettingsEvent.OpenFileManager) },
                    )
                }
            }

            Spacer(Modifier.height(8.dp))

            // ── Preferences ────────────────────────────────────────────────
            SectionHeader(stringResource(R.string.settings_preferences))

            val odometerLabel =
                if (state.defaultOdometerUnit == OdometerUnit.KM) {
                    stringResource(R.string.settings_unit_km)
                } else {
                    stringResource(R.string.settings_unit_mi)
                }

            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 4.dp, bottomEnd = 4.dp),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                ListItem(
                    headlineContent = { Text(stringResource(R.string.settings_odometer_unit)) },
                    supportingContent = { Text(odometerLabel) },
                    leadingContent = {
                        SettingIcon(
                            icon = Icons.Default.Speed,
                            containerColor = MaterialTheme.colorScheme.secondaryContainer,
                            tint = MaterialTheme.colorScheme.onSecondaryContainer,
                        )
                    },
                    modifier = Modifier.clickable { showOdometerDialog = true },
                )
            }
            Spacer(Modifier.height(2.dp))
            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 24.dp, bottomEnd = 24.dp),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                ListItem(
                    headlineContent = { Text(stringResource(R.string.settings_currency)) },
                    supportingContent = { Text(state.defaultCurrency) },
                    leadingContent = {
                        SettingIcon(
                            icon = Icons.Default.AttachMoney,
                            containerColor = MaterialTheme.colorScheme.secondaryContainer,
                            tint = MaterialTheme.colorScheme.onSecondaryContainer,
                        )
                    },
                    modifier = Modifier.clickable { showCurrencyDialog = true },
                )
            }

            Spacer(Modifier.height(8.dp))

            // ── Organization ───────────────────────────────────────────────
            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(24.dp),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                ListItem(
                    headlineContent = { Text(stringResource(R.string.settings_categories)) },
                    leadingContent = {
                        SettingIcon(
                            icon = Icons.Default.Category,
                            containerColor = MaterialTheme.colorScheme.secondaryContainer,
                            tint = MaterialTheme.colorScheme.onSecondaryContainer,
                        )
                    },
                    trailingContent = {
                        Icon(Icons.AutoMirrored.Outlined.ArrowForwardIos, contentDescription = null)
                    },
                    modifier = Modifier.clickable { viewModel.onEvent(SettingsEvent.OpenCategories) },
                )
            }

            Spacer(Modifier.height(8.dp))

            // ── Premium ──────────────────────────────────────────────────
            SectionHeader(stringResource(R.string.settings_premium_title))

            val premiumCardContainerColor =
                if (state.isPremium) {
                    paywallContainerColor(isDark)
                } else {
                    MaterialTheme.colorScheme.surfaceContainerLow
                }

            val premiumCardContentColor =
                if (state.isPremium) {
                    paywallContentColor(isDark)
                } else {
                    MaterialTheme.colorScheme.onSurface
                }

            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 4.dp, bottomEnd = 4.dp),
                colors = CardDefaults.cardColors(containerColor = premiumCardContainerColor),
            ) {
                if (state.isPremium) {
                    ListItem(
                        headlineContent = { Text(stringResource(R.string.premium_active_title)) },
                        supportingContent = { Text(stringResource(R.string.premium_active_subtitle)) },
                        leadingContent = {
                            SettingIcon(
                                icon = Icons.Default.Star,
                                containerColor =
                                    if (isDark) {
                                        MaterialTheme.colorScheme.tertiary.copy(alpha = 0.4f)
                                    } else {
                                        MaterialTheme.colorScheme.tertiary
                                    },
                                tint =
                                    if (isDark) {
                                        MaterialTheme.colorScheme.onSurface
                                    } else {
                                        MaterialTheme.colorScheme.onTertiary
                                    },
                            )
                        },
                        colors =
                            ListItemDefaults.colors(
                                containerColor = Color.Transparent,
                                headlineColor = premiumCardContentColor,
                                supportingColor = premiumCardContentColor.copy(alpha = 0.8f),
                            ),
                    )
                } else {
                    ListItem(
                        headlineContent = { Text(stringResource(R.string.upgrade_cta_title)) },
                        supportingContent = { Text(stringResource(R.string.upgrade_cta_subtitle)) },
                        leadingContent = {
                            SettingIcon(
                                icon = Icons.Default.Star,
                                containerColor = MaterialTheme.colorScheme.primaryContainer,
                                tint = MaterialTheme.colorScheme.onPrimaryContainer,
                            )
                        },
                        modifier = Modifier.clickable { viewModel.onEvent(SettingsEvent.RequestUpgrade) },
                    )
                }
            }
            Spacer(Modifier.height(2.dp))
            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 24.dp, bottomEnd = 24.dp),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                ListItem(
                    headlineContent = { Text(stringResource(R.string.restore_button)) },
                    leadingContent = {
                        SettingIcon(
                            icon = Icons.Default.Info,
                            containerColor = MaterialTheme.colorScheme.surfaceVariant,
                            tint = MaterialTheme.colorScheme.onSurfaceVariant,
                        )
                    },
                    trailingContent = {
                        if (state.isRestoringPurchases) {
                            CircularProgressIndicator(modifier = Modifier.size(24.dp))
                        }
                    },
                    modifier =
                        Modifier.clickable(enabled = !state.isRestoringPurchases) {
                            viewModel.onEvent(SettingsEvent.RestorePurchases)
                        },
                )
            }

            Spacer(Modifier.height(8.dp))

            // ── Developer ──────────────────────────────────────────────────
            if (BuildConfig.DEBUG) {
                SectionHeader(stringResource(R.string.settings_developer_title))

                Card(
                    modifier = Modifier.fillMaxWidth(),
                    shape = RoundedCornerShape(24.dp),
                    colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
                ) {
                    Column(modifier = Modifier.padding(vertical = 8.dp)) {
                        Text(
                            text = stringResource(R.string.settings_billing_override_title),
                            style = MaterialTheme.typography.labelMedium,
                            color = MaterialTheme.colorScheme.primary,
                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
                        )

                        listOf<Pair<DebugBillingOverrideMode, String>>(
                            DebugBillingOverrideMode.NONE to stringResource(R.string.settings_billing_override_real),
                            DebugBillingOverrideMode.FORCE_FREE to stringResource(R.string.settings_billing_override_free),
                            DebugBillingOverrideMode.FORCE_PREMIUM to stringResource(R.string.settings_billing_override_premium),
                            DebugBillingOverrideMode.FORCE_PENDING to stringResource(R.string.settings_billing_override_pending),
                        ).forEach { (mode, label) ->
                            Row(
                                verticalAlignment = Alignment.CenterVertically,
                                modifier =
                                    Modifier
                                        .fillMaxWidth()
                                        .clickable { viewModel.onEvent(SettingsEvent.SetDebugOverride(mode)) }
                                        .padding(horizontal = 16.dp),
                            ) {
                                RadioButton(
                                    selected = state.debugOverrideMode == mode,
                                    onClick = { viewModel.onEvent(SettingsEvent.SetDebugOverride(mode)) },
                                )
                                Text(label, style = MaterialTheme.typography.bodyMedium)
                            }
                        }

                        TextButton(
                            onClick = { viewModel.onEvent(SettingsEvent.SimulatePurchaseSuccess) },
                            modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
                        ) {
                            Text(stringResource(R.string.settings_simulate_purchase_success))
                        }
                        TextButton(
                            onClick = { viewModel.onEvent(SettingsEvent.ResetBillingState) },
                            modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
                        ) {
                            Text(
                                text = stringResource(R.string.settings_reset_billing_state),
                                color = MaterialTheme.colorScheme.error,
                            )
                        }

                        Text(
                            text = stringResource(R.string.settings_crash_reporting_dev_title),
                            style = MaterialTheme.typography.labelMedium,
                            color = MaterialTheme.colorScheme.primary,
                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
                        )

                        if (showCrashConfirmation) {
                            AlertDialog(
                                onDismissRequest = { showCrashConfirmation = false },
                                title = { Text(stringResource(R.string.settings_force_test_crash_dialog_title)) },
                                text = { Text(stringResource(R.string.settings_force_test_crash_dialog_message)) },
                                confirmButton = {
                                    TextButton(
                                        onClick = {
                                            showCrashConfirmation = false
                                            @Suppress("TooGenericExceptionThrown")
                                            throw RuntimeException("Yorvana dev test crash")
                                        },
                                    ) {
                                        Text(
                                            text = stringResource(R.string.settings_force_test_crash_dialog_confirm),
                                            color = MaterialTheme.colorScheme.error,
                                        )
                                    }
                                },
                                dismissButton = {
                                    TextButton(onClick = { showCrashConfirmation = false }) {
                                        Text(stringResource(android.R.string.cancel))
                                    }
                                },
                            )


sed -n '620,980p' app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt


Output:
Chunk ID: d4fc8e
Wall time: 0.1106 seconds
Process exited with code 0
Original token count: 1939
Output:
                            )
                        }

                        OutlinedButton(
                            onClick = { showCrashConfirmation = true },
                            modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
                        ) {
                            Text(
                                text = stringResource(R.string.settings_force_test_crash),
                                color = MaterialTheme.colorScheme.error,
                            )
                        }
                    }
                }
                Spacer(Modifier.height(8.dp))
            }

            Spacer(Modifier.height(8.dp))

            // ── Privacy ───────────────────────────────────────────────────
            SectionHeader(stringResource(R.string.settings_privacy))

            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(24.dp),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                Column {
                    ListItem(
                        headlineContent = { Text(stringResource(R.string.settings_crash_reporting)) },
                        supportingContent = { Text(stringResource(R.string.settings_crash_reporting_subtitle)) },
                        leadingContent = {
                            SettingIcon(
                                icon = Icons.Default.BugReport,
                                containerColor = MaterialTheme.colorScheme.primaryContainer,
                                tint = MaterialTheme.colorScheme.onPrimaryContainer,
                            )
                        },
                        trailingContent = {
                            Switch(
                                checked = state.crashReportingEnabled,
                                onCheckedChange = { viewModel.onEvent(SettingsEvent.CrashReportingToggled(it)) },
                                modifier = Modifier.testTag(TestTags.SETTINGS_CRASH_REPORTING_TOGGLE),
                            )
                        },
                    )

                    Row(
                        modifier =
                            Modifier
                                .fillMaxWidth()
                                .padding(start = 56.dp, end = 8.dp, top = 4.dp, bottom = 8.dp),
                        verticalAlignment = Alignment.CenterVertically,
                    ) {
                        TextButton(
                            onClick = { showPrivacyDialog = true },
                            modifier = Modifier.testTag(TestTags.PRIVACY_LEARN_MORE),
                        ) {
                            Text(stringResource(R.string.action_learn_more))
                        }
                    }
                }
            }

            Spacer(Modifier.height(8.dp))

            // ── Feedback ──────────────────────────────────────────────────
            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(24.dp),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                ListItem(
                    headlineContent = { Text(stringResource(R.string.settings_report_bug)) },
                    leadingContent = {
                        SettingIcon(
                            icon = Icons.Default.BugReport,
                            containerColor = MaterialTheme.colorScheme.primaryContainer,
                            tint = MaterialTheme.colorScheme.onPrimaryContainer,
                        )
                    },
                    trailingContent = {
                        Icon(Icons.AutoMirrored.Outlined.ArrowForwardIos, contentDescription = null)
                    },
                    modifier = Modifier.clickable { viewModel.onEvent(SettingsEvent.OpenReportBug) },
                )
            }

            Spacer(Modifier.height(8.dp))

            // ── About ──────────────────────────────────────────────────────
            SectionHeader(stringResource(R.string.settings_about))

            Card(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(24.dp),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
            ) {
                ListItem(
                    headlineContent = { Text(stringResource(R.string.app_name)) },
                    supportingContent = {
                        Text(stringResource(R.string.settings_version, state.appVersion))
                    },
                    leadingContent = {
                        SettingIcon(
                            icon = Icons.Default.Info,
                            containerColor = MaterialTheme.colorScheme.surfaceVariant,
                            tint = MaterialTheme.colorScheme.onSurfaceVariant,
                        )
                    },
                )
            }

            Spacer(Modifier.height(8.dp))
        }
    }
}

@Composable
private fun OdometerUnitDialog(
    current: OdometerUnit,
    onSelect: (OdometerUnit) -> Unit,
    onDismiss: () -> Unit,
) {
    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text(stringResource(R.string.settings_odometer_unit)) },
        text = {
            Column {
                OdometerUnit.entries.forEach { unit ->
                    val label =
                        if (unit == OdometerUnit.KM) {
                            stringResource(R.string.settings_unit_km)
                        } else {
                            stringResource(R.string.settings_unit_mi)
                        }
                    Row(
                        verticalAlignment = Alignment.CenterVertically,
                        modifier =
                            Modifier
                                .fillMaxWidth()
                                .clickable { onSelect(unit) },
                    ) {
                        RadioButton(
                            selected = unit == current,
                            onClick = { onSelect(unit) },
                        )
                        Text(label, style = MaterialTheme.typography.bodyLarge)
                    }
                }
            }
        },
        confirmButton = {},
        dismissButton = {
            TextButton(onClick = onDismiss) {
                Text(stringResource(R.string.action_cancel))
            }
        },
    )
}

@Composable
private fun SectionHeader(title: String) {
    Text(
        text = title,
        style = MaterialTheme.typography.labelLarge,
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 12.dp, bottom = 8.dp),
    )
}

@Composable
private fun SettingIcon(
    icon: ImageVector,
    containerColor: Color,
    tint: Color,
) {
    Surface(
        shape = CircleShape,
        color = containerColor,
        modifier = Modifier.size(40.dp),
    ) {
        Icon(
            imageVector = icon,
            contentDescription = null,
            tint = tint,
            modifier =
                Modifier
                    .padding(8.dp)
                    .fillMaxSize(),
        )
    }
}


sed -n '1,240p' app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt


sed -n '1,220p' app/src/main/java/com/yorvana/ui/reportbug/ReportBugViewModel.kt


sed -n '80,150p' app/src/main/res/values/strings.xml


sed -n '1,220p' app/src/test/java/com/yorvana/ui/settings/SettingsScreenIT.kt


Output:
Chunk ID: 75aa3e
Wall time: 0.3768 seconds
Process exited with code 0
Original token count: 2605
Output:
package com.yorvana.ui.reportbug

import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.ui.TestTags
import com.yorvana.ui.components.TopBarBackButton
import com.yorvana.ui.theme.appTopAppBarColors
import com.yorvana.ui.util.ViewModelFactory

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportBugScreen(
    backStack: NavBackStack<NavKey>,
    viewModel: ReportBugViewModel =
        viewModel(
            factory = ViewModelFactory(LocalContext.current.applicationContext as YorvanaApplication),
        ),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    val context = LocalContext.current
    val snackbarHostState = remember { SnackbarHostState() }
    val scrollState = rememberScrollState()

    var showDeviceInfo by rememberSaveable { mutableStateOf(false) }

    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)

    LaunchedEffect(Unit) {
        viewModel.effects.collect { effect ->
            when (effect) {
                is ReportBugEffect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message)
                ReportBugEffect.NavigateBack -> backStack.removeLastOrNull()
                is ReportBugEffect.FallbackToEmail -> {
                    val intent =
                        Intent(Intent.ACTION_SENDTO).apply {
                            data = "mailto:".toUri()
                            putExtra(Intent.EXTRA_EMAIL, arrayOf(effect.recipient))
                            putExtra(Intent.EXTRA_SUBJECT, effect.subject)
                            putExtra(Intent.EXTRA_TEXT, effect.body)
                        }
                    try {
                        context.startActivity(intent)
                    } catch (_: Exception) {
                        snackbarHostState.showSnackbar(noEmailAppMessage)
                    }
                }
            }
        }
    }

    if (showDeviceInfo) {
        DeviceInfoDialog(
            state = state,
            onIncludeChanged = { viewModel.onEvent(ReportBugEvent.IncludeDeviceInfoChanged(it)) },
            onDismissRequest = { showDeviceInfo = false },
        )
    }

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) },
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = stringResource(R.string.report_bug_title),
                        modifier = Modifier.testTag(TestTags.SCREEN_TITLE),
                    )
                },
                navigationIcon = {
                    TopBarBackButton(
                        onClick = { backStack.removeLastOrNull() },
                        enabled = !state.isSubmitting,
                    )
                },
                actions = {
                    if (state.isSubmitting) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(24.dp).padding(end = 16.dp).testTag(TestTags.REPORT_BUG_LOADING),
                            strokeWidth = 2.dp,
                        )
                    } else {
                        TextButton(
                            onClick = { viewModel.onEvent(ReportBugEvent.Submit) },
                            enabled = state.canSubmit,
                            modifier = Modifier.testTag(TestTags.REPORT_BUG_SUBMIT_BUTTON),
                        ) {
                            Text(stringResource(R.string.report_bug_submit))
                        }
                    }
                },
                colors = appTopAppBarColors(),
            )
        },
    ) { padding ->
        Column(
            modifier =
                Modifier
                    .fillMaxSize()
                    .padding(padding)
                    .imePadding()
                    .verticalScroll(scrollState)
                    .padding(horizontal = 16.dp, vertical = 12.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            if (state.isSubmitting) {
                LinearProgressIndicator(
                    modifier = Modifier.fillMaxWidth(),
                )
            }

            // Type
            Text(
                text = stringResource(R.string.report_bug_type_section),
                style = MaterialTheme.typography.titleSmall,
                color = MaterialTheme.colorScheme.primary,
            )

            SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
                SegmentedButton(
                    selected = state.type == ReportType.BUG,
                    onClick = { viewModel.onEvent(ReportBugEvent.TypeChanged(ReportType.BUG)) },
                    shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
                    enabled = !state.isSubmitting,
                    label = { Text(stringResource(R.string.report_bug_type_bug)) },
                    modifier = Modifier.testTag(TestTags.REPORT_BUG_TYPE_BUG),
                )
                SegmentedButton(
                    selected = state.type == ReportType.SUGGESTION,
                    onClick = { viewModel.onEvent(ReportBugEvent.TypeChanged(ReportType.SUGGESTION)) },
                    shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
                    enabled = !state.isSubmitting,
                    label = { Text(stringResource(R.string.report_bug_type_suggestion)) },
                    modifier = Modifier.testTag(TestTags.REPORT_BUG_TYPE_SUGGESTION),
                )
            }

            OutlinedTextField(
                value = state.summary,
                onValueChange = { viewModel.onEvent(ReportBugEvent.SummaryChanged(it)) },
                label = { Text(stringResource(R.string.report_bug_summary)) },
                modifier = Modifier.fillMaxWidth().testTag(TestTags.REPORT_BUG_SUMMARY_FIELD),
                singleLine = true,
                enabled = !state.isSubmitting,
                supportingText = {
                    Text(
                        "${state.summary.length}/${ReportBugViewModel.MAX_SUMMARY_LENGTH}",
                        modifier = Modifier.fillMaxWidth(),
                    )
                },
            )

            OutlinedTextField(
                value = state.description,
                onValueChange = { viewModel.onEvent(ReportBugEvent.DescriptionChanged(it)) },
                label = { Text(stringResource(R.string.report_bug_description)) },
                modifier =
                    Modifier
                        .fillMaxWidth()
                        .height(160.dp)
                        .testTag(TestTags.REPORT_BUG_DESCRIPTION_FIELD),
                enabled = !state.isSubmitting,
                supportingText = {
                    Text(
                        "${state.description.length}/${ReportBugViewModel.MAX_DESCRIPTION_LENGTH}",
                        modifier = Modifier.fillMaxWidth(),
                    )
                },
            )

            OutlinedTextField(
                value = state.contactEmail,
                onValueChange = { viewModel.onEvent(ReportBugEvent.ContactEmailChanged(it)) },
                label = { Text(stringResource(R.string.report_bug_contact_email)) },
                modifier = Modifier.fillMaxWidth().testTag(TestTags.REPORT_BUG_EMAIL_FIELD),
                singleLine = true,
                enabled = !state.isSubmitting,
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
                isError = state.isEmailInvalid,
                supportingText =
                    if (state.isEmailInvalid) {
                        { Text(stringResource(R.string.report_bug_contact_email_invalid)) }
                    } else {
                        null
                    },
            )

            Card(
                modifier = Modifier.fillMaxWidth(),


Output:
Chunk ID: 3d2a36
Wall time: 0.3732 seconds
Process exited with code 0
Original token count: 2056
Output:
package com.yorvana.ui.reportbug

import android.os.Build
import android.util.Log
import android.util.Patterns
import androidx.lifecycle.viewModelScope
import com.yorvana.BuildConfig
import com.yorvana.R
import com.yorvana.ui.util.GatedViewModel
import com.yorvana.ui.util.ViewModelDependencies
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale

enum class ReportType { BUG, SUGGESTION }

data class ReportBugState(
    val type: ReportType = ReportType.BUG,
    val summary: String = "",
    val description: String = "",
    val contactEmail: String = "",
    val includeDeviceInfo: Boolean = true,
    val isSubmitting: Boolean = false,
    val appVersion: String = "",
    val androidVersion: String = "",
    val locale: String = "",
    val deviceModel: String = "",
    val vaultConfigured: Boolean = false,
    val crashReportingEnabled: Boolean = true,
) {
    /**
     * NOTE: We use Patterns.EMAIL_ADDRESS but add a requirement for a dot in the domain
     * to be robust to Patterns.EMAIL_ADDRESS behavior across API levels.
     */
    val isEmailInvalid: Boolean get() =
        contactEmail.trim().let { trimmed ->
            trimmed.isNotBlank() && (
                !Patterns.EMAIL_ADDRESS.matcher(trimmed).matches() ||
                    !trimmed.substringAfter('@', "").contains('.')
            )
        }
    val canSubmit: Boolean get() = summary.isNotBlank() && description.isNotBlank() && !isSubmitting && !isEmailInvalid
}

sealed class ReportBugEvent {
    data class TypeChanged(val type: ReportType) : ReportBugEvent()

    data class SummaryChanged(val summary: String) : ReportBugEvent()

    data class DescriptionChanged(val description: String) : ReportBugEvent()

    data class ContactEmailChanged(val email: String) : ReportBugEvent()

    data class IncludeDeviceInfoChanged(val include: Boolean) : ReportBugEvent()

    data object Submit : ReportBugEvent()
}

sealed class ReportBugEffect {
    data class ShowSnackbar(val message: String) : ReportBugEffect()

    data object NavigateBack : ReportBugEffect()

    data class FallbackToEmail(
        val recipient: String,
        val subject: String,
        val body: String,
    ) : ReportBugEffect()
}

class ReportBugViewModel(
    dependencies: ViewModelDependencies,
) : GatedViewModel(dependencies) {
    private val sentryWrapper = dependencies.sentryWrapper
    private val _state = MutableStateFlow(ReportBugState())
    val state: StateFlow<ReportBugState> = _state.asStateFlow()

    private val _effects = Channel<ReportBugEffect>(Channel.BUFFERED)
    val effects = _effects.receiveAsFlow()

    private var retryCount = 0

    init {
        _state.update {
            it.copy(
                appVersion = BuildConfig.VERSION_NAME,
                androidVersion = Build.VERSION.RELEASE,
                locale = Locale.getDefault().toString(),
                deviceModel = Build.MODEL,
            )
        }
        viewModelScope.launch {
            dependencies.preferences.preferences.collect { prefs ->
                _state.update {
                    it.copy(
                        vaultConfigured = !prefs.vaultUriString.isNullOrEmpty(),
                        crashReportingEnabled = prefs.crashReportingEnabled,
                    )
                }
            }
        }
    }

    fun onEvent(event: ReportBugEvent) {
        when (event) {
            is ReportBugEvent.TypeChanged -> _state.update { it.copy(type = event.type) }
            is ReportBugEvent.SummaryChanged -> _state.update { it.copy(summary = event.summary.take(MAX_SUMMARY_LENGTH)) }
            is ReportBugEvent.DescriptionChanged -> _state.update { it.copy(description = event.description.take(MAX_DESCRIPTION_LENGTH)) }
            is ReportBugEvent.ContactEmailChanged -> _state.update { it.copy(contactEmail = event.email) }
            is ReportBugEvent.IncludeDeviceInfoChanged -> _state.update { it.copy(includeDeviceInfo = event.include) }
            ReportBugEvent.Submit -> {
                retryCount = 0
                submitReport()
            }
        }
    }

    private fun submitReport() {
        if (!state.value.canSubmit) return

        _state.update { it.copy(isSubmitting = true) }
        viewModelScope.launch {
            performSubmission()
        }
    }

    private suspend fun performSubmission() {
        try {
            val s = _state.value
            val comments = "${s.summary}\n\n${s.description}"
            val email = s.contactEmail.trim().ifBlank { null }

            val tags =
                mapOf(
                    "type" to s.type.name.lowercase(),
                    "app_version" to s.appVersion,
                    "android_version" to s.androidVersion,
                    "locale" to s.locale,
                    "device_model" to s.deviceModel,
                    "vault_configured" to s.vaultConfigured.toString(),
                )

            val submissionTags = if (s.includeDeviceInfo) tags else emptyMap()

            withContext(Dispatchers.IO) {
                if (sentryWrapper.isEnabled()) {
                    sentryWrapper.captureFeedback(comments, email, submissionTags)
                } else {
                    // NOTE: bypasses crash-reporting opt-out; user-initiated feedback submission.
                    sentryWrapper.lazyInitAndSendFeedback(
                        app,
                        comments,
                        email,
                        submissionTags,
                    )
                }
            }

            _state.update { it.copy(isSubmitting = false) }
            _effects.send(ReportBugEffect.NavigateBack)
        } catch (
            @Suppress("TooGenericExceptionCaught") e: Exception,
        ) {
            Log.e("ReportBugViewModel", "Failed to submit report", e)
            if (retryCount < MAX_RETRY_COUNT) {
                retryCount++
                delay(RETRY_DELAY_MS)
                performSubmission()
            } else {
                _state.update { it.copy(isSubmitting = false) }
                _effects.send(ReportBugEffect.ShowSnackbar(app.getString(R.string.report_bug_error_failed)))
                fallbackToEmail()
            }
        }
    }

    private suspend fun fallbackToEmail() {
        val s = _state.value
        val reportType =
            when (s.type) {
                ReportType.BUG -> app.getString(R.string.report_bug_type_bug)
                ReportType.SUGGESTION -> app.getString(R.string.report_bug_type_suggestion)
            }

        val subject = "[Yorvana] $reportType: ${s.summary}"
        val body =
            buildString {
                appendLine(s.description)
                if (s.contactEmail.isNotBlank()) {
                    appendLine()
                    appendLine("${app.getString(R.string.report_bug_email_contact_label)}: ${s.contactEmail}")
                }
                if (s.includeDeviceInfo) {
                    appendLine()
                    appendLine(app.getString(R.string.report_bug_email_device_info))
                    appendLine("${app.getString(R.string.report_bug_app_version)}: ${s.appVersion}")
                    appendLine("${app.getString(R.string.report_bug_android_version)}: ${s.androidVersion}")
                    appendLine("${app.getString(R.string.report_bug_locale)}: ${s.locale}")
                    appendLine("${app.getString(R.string.report_bug_device_model)}: ${s.deviceModel}")
                    appendLine("${app.getString(R.string.report_bug_vault_configured)}: ${s.vaultConfigured}")
                }
            }
        _effects.send(
            ReportBugEffect.FallbackToEmail(
                app.getString(R.string.report_bug_support_email),
                subject,
                body,
            ),
        )
    }

    companion object {


Output:
Chunk ID: 07dcaf
Wall time: 0.3865 seconds
Process exited with code 0
Original token count: 1345
Output:

    <!-- Settings -->
    <string name="settings_title">Settings</string>
    <string name="settings_premium_title">Premium</string>
    <string name="settings_vault">Storage</string>
    <string name="settings_vault_location">Vault folder</string>
    <string name="settings_vault_not_set">Not configured</string>
    <string name="settings_vault_change">Change folder</string>
    <string name="settings_vault_move_title">Move vault data?</string>
    <string name="settings_vault_move_message">Would you like to move your existing data to the new folder, or start fresh?</string>
    <string name="settings_vault_move_action">Move data</string>
    <string name="settings_vault_fresh_action">Start fresh</string>
    <string name="settings_vault_moving">Moving vault…</string>
    <string name="settings_vault_move_error">Failed to move vault: %s</string>
    <string name="settings_vault_changed_success">Vault folder successfully changed.</string>
    <string name="settings_vault_conflict_title">Folder Not Empty</string>
    <string name="settings_vault_conflict_message">Cannot move vault: The selected folder is not empty and is not a Yorvana vault. Please choose an empty folder.</string>
    <string name="settings_vault_existing_title">Existing Vault Found</string>
    <string name="settings_vault_existing_message">This folder already contains Yorvana data. Would you like to use this existing vault?</string>
    <string name="settings_vault_use_existing_action">Use this vault</string>
    <string name="settings_vault_validating">Validating folder…</string>
    <string name="settings_preferences">Preferences</string>
    <string name="settings_odometer_unit">Default odometer unit</string>
    <string name="settings_currency">Default currency</string>
    <string name="settings_categories">Service categories</string>
    <string name="settings_unit_km">Kilometres (km)</string>
    <string name="settings_unit_mi">Miles (mi)</string>
    <string name="settings_explore_data">Explore Data</string>
    <string name="settings_explore_data_subtitle">Open vault in system file manager</string>
    <string name="settings_about">About</string>
    <string name="settings_report_bug">Report a bug or suggest a feature</string>
    <string name="settings_version">Version %s</string>

    <!-- Report Bug -->
    <string name="report_bug_title">Feedback</string>
    <string name="report_bug_type_bug">Bug</string>
    <string name="report_bug_type_suggestion">Suggestion</string>
    <string name="report_bug_summary">Summary</string>
    <string name="report_bug_description">Description</string>
    <string name="report_bug_contact_email">Contact email (optional)</string>
    <string name="report_bug_contact_email_invalid">Please enter a valid email address</string>
    <string name="report_bug_email_contact_label">Contact email</string>
    <string name="report_bug_type_section">Type</string>
    <string name="report_bug_no_email_app">No email app found</string>
    <string name="report_bug_include_device_info">Include additional app state</string>
    <string name="report_bug_device_info_payload">Device information</string>
    <string name="report_bug_sentry_opt_out_disclosure">Submitting will connect to our error reporting service for this report only. Basic device info is always attached.</string>
    <string name="report_bug_email_device_info">--- Device Info ---</string>
    <string name="report_bug_submit">Submit</string>
    <string name="report_bug_error_failed">Failed to send report after retries</string>
    <string name="report_bug_vault_configured">Vault Configured</string>
    <string name="report_bug_app_version">App Version</string>
    <string name="report_bug_android_version">Android Version</string>
    <string name="report_bug_locale">Locale</string>
    <string name="report_bug_device_model">Device Model</string>
    <string name="report_bug_support_email" translatable="false">yorvana.app@gmail.com</string>
    <string name="settings_file_manager_error">No app found to open this location</string>
    <string name="settings_currency_search">Search currencies</string>
    <string name="settings_privacy">Privacy</string>
    <string name="settings_crash_reporting">Help improve the app</string>
    <string name="settings_crash_reporting_subtitle">Share anonymous crash logs to help us fix bugs.</string>
    <string name="settings_privacy_description">We use Sentry to collect anonymous technical data when the app crashes or behaves unexpectedly. This data helps us identify and fix issues more quickly.</string>
    <string name="settings_privacy_captured_title">What is captured:</string>
    <string name="settings_privacy_captured_list">• App version\n• Android version & device model\n• System locale\n• Stack traces (technical error details)</string>
    <string name="settings_privacy_never_captured_title">What is NEVER captured:</string>
    <string name="settings_privacy_never_captured_list">• Your vehicles or service records\n• Attachment names or content\n• Any personal information</string>
    <string name="action_learn_more">Learn more</string>
    <string name="action_show_less">Show less</string>
    <string name="action_show_more">Show more</string>
    <string name="action_view_more">View more</string>
    <string name="a11y_state_expanded">Expanded</string>