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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Project Guidelines

## Commit & PR Rules

- Do NOT include `Co-Authored-By` lines referencing Codex in commit messages.
- Do NOT include "Generated with Codex" or similar AI attribution in PR descriptions.
- Do NOT use a `codex/` branch prefix. Follow the existing type-based convention, such as `feat/`, `fix/`, `chore/`, `docs/`, or `refactor/`.
- **Keep commit messages compact.** Subject ≤ 72 chars. Body only when the "why" isn't obvious from the diff — 1–3 short lines, no multi-paragraph essays. Don't restate what the diff already shows.

## Code Style

- Do NOT use fully-qualified names (FQN) inline in Kotlin code. Always add a proper `import` statement at the top of the file and reference the type by its simple name. This applies to both production and test code.
- **Keep comments compact.** Explain non-obvious intent in one or two lines. No multi-paragraph KDoc unless the API is genuinely complex. Skip comments that just restate the code.

## Code Style

- Do NOT use fully-qualified names (FQN) inline in Kotlin code. Always add a proper `import` statement at the top of the file and reference the type by its simple name. This applies to both production and test code.

## Issues

- Write all issues in **English**.
- Use clear section headers: `## Summary`, `## Problem` / `## Motivation`, `## Proposed Approach` / `## Proposed Behavior`, etc.
- Include relevant code snippets, color values, or architecture details where helpful.

## Release

- Always run `./gradlew clean` before `publishAllPublicationsToMavenCentralRepository` to ensure freshly compiled artifacts are uploaded (not stale build cache).

## Skills

- Code review follow-up: use the `/resolve-coderabbit-review` skill to triage and apply CodeRabbit comments on the current PR.
- Release: use the `/release` skill to publish to Maven Central and draft GitHub release notes.
8 changes: 8 additions & 0 deletions dari/src/main/kotlin/com/easyhooon/dari/Dari.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ object Dari {
preferences.setDarkMode(value)
}

/**
* Enable or disable controls for folding nested JSON objects and arrays.
* Persists across process restarts.
*/
fun setJsonFoldingEnabled(enabled: Boolean) {
preferences.setJsonFoldingEnabled(enabled)
}

private fun applyShakeToOpen(enabled: Boolean) {
shakeManager?.unregister()
shakeManager = if (enabled) {
Expand Down
27 changes: 22 additions & 5 deletions dari/src/main/kotlin/com/easyhooon/dari/data/DariPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
* Persists user-toggled Dari settings (shake-to-open, dark mode) so changes
* Persists user-toggled Dari settings so changes
* survive process restarts and override the initial [com.easyhooon.dari.DariConfig]
* defaults.
*
Expand All @@ -41,22 +41,24 @@ internal class DariPreferences(
/** User override for dark mode; `null` means "follow the system theme". */
private val _darkMode = MutableStateFlow<Boolean?>(null)

private val _jsonFoldingEnabled = MutableStateFlow(true)

init {
// One-shot blocking read so [shakeToOpen] and [darkMode] are correct
// for the very first synchronous caller (e.g. Dari.init's shake
// registration, or Compose initialValue). DataStore file is tiny so
// this takes ~a few ms on cold start.
// One-shot blocking read so settings are correct for the first synchronous
// caller (e.g. Dari.init or Compose initialValue).
runBlocking {
val snapshot = dataStore.data.first()
_shakeToOpen.value = snapshot[KEY_SHAKE_TO_OPEN] ?: defaultShakeToOpen
_darkMode.value = snapshot[KEY_DARK_MODE]
_jsonFoldingEnabled.value = snapshot[KEY_JSON_FOLDING_ENABLED] ?: true
}

// Keep the StateFlows in sync with any subsequent DataStore writes.
scope.launch {
dataStore.data.collect { prefs ->
_shakeToOpen.value = prefs[KEY_SHAKE_TO_OPEN] ?: defaultShakeToOpen
_darkMode.value = prefs[KEY_DARK_MODE]
_jsonFoldingEnabled.value = prefs[KEY_JSON_FOLDING_ENABLED] ?: true
}
}
}
Expand Down Expand Up @@ -95,8 +97,23 @@ internal class DariPreferences(

// endregion

// region JSON folding

val jsonFoldingEnabled: Boolean get() = _jsonFoldingEnabled.value

fun jsonFoldingEnabledFlow(): Flow<Boolean> = _jsonFoldingEnabled.asStateFlow()

fun setJsonFoldingEnabled(value: Boolean) {
scope.launch {
dataStore.edit { it[KEY_JSON_FOLDING_ENABLED] = value }
}
}

// endregion

companion object {
private val KEY_SHAKE_TO_OPEN = booleanPreferencesKey("shake_to_open")
private val KEY_DARK_MODE = booleanPreferencesKey("dark_mode")
private val KEY_JSON_FOLDING_ENABLED = booleanPreferencesKey("json_folding_enabled")
}
}
6 changes: 6 additions & 0 deletions dari/src/main/kotlin/com/easyhooon/dari/ui/DariActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ class DariActivity : ComponentActivity() {
val shakeToOpen by Dari.preferences.shakeToOpenFlow().collectAsStateWithLifecycle(
initialValue = Dari.preferences.shakeToOpen,
)
val jsonFoldingEnabled by Dari.preferences.jsonFoldingEnabledFlow()
.collectAsStateWithLifecycle(
initialValue = Dari.preferences.jsonFoldingEnabled,
)
var isSearchMode by rememberSaveable { mutableStateOf(false) }
var searchQuery by rememberSaveable { mutableStateOf("") }
var selectedTag by rememberSaveable { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -396,6 +400,8 @@ class DariActivity : ComponentActivity() {
onShakeToOpenChange = { Dari.setShakeToOpenEnabled(it) },
darkMode = darkMode,
onDarkModeChange = { Dari.setDarkMode(it) },
jsonFoldingEnabled = jsonFoldingEnabled,
onJsonFoldingEnabledChange = { Dari.setJsonFoldingEnabled(it) },
onClearMessages = {
showSettingsSheet = false
showClearDialog = true
Expand Down
37 changes: 27 additions & 10 deletions dari/src/main/kotlin/com/easyhooon/dari/ui/DariDetailActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ class DariDetailActivity : ComponentActivity() {
val darkMode by Dari.preferences.darkModeFlow().collectAsStateWithLifecycle(
initialValue = Dari.preferences.darkMode,
)
val jsonFoldingEnabled by Dari.preferences.jsonFoldingEnabledFlow()
.collectAsStateWithLifecycle(
initialValue = Dari.preferences.jsonFoldingEnabled,
)
val isDark = darkMode ?: isSystemInDarkTheme()
ApplyDariSystemBars(isDark)
DariTheme(darkTheme = darkMode) {
Expand Down Expand Up @@ -199,7 +203,7 @@ class DariDetailActivity : ComponentActivity() {
if (entry == null) {
Text("Message not found", modifier = Modifier.padding(16.dp))
} else {
DetailTabs(entry)
DetailTabs(entry, jsonFoldingEnabled)
}
}

Expand All @@ -212,7 +216,10 @@ class DariDetailActivity : ComponentActivity() {
private val TAB_TITLES = listOf("OVERVIEW", "REQUEST", "RESPONSE")

@Composable
private fun DetailTabs(entry: MessageEntry) {
private fun DetailTabs(
entry: MessageEntry,
jsonFoldingEnabled: Boolean,
) {
val pagerState = rememberPagerState(pageCount = { TAB_TITLES.size })
val coroutineScope = rememberCoroutineScope()

Expand Down Expand Up @@ -255,8 +262,8 @@ private fun DetailTabs(entry: MessageEntry) {
) { page ->
when (page) {
0 -> OverviewTab(entry)
1 -> DataTab(entry.requestData, entry.requestPayloadMetadata)
2 -> DataTab(entry.responseData, entry.responsePayloadMetadata)
1 -> DataTab(entry.requestData, entry.requestPayloadMetadata, jsonFoldingEnabled)
2 -> DataTab(entry.responseData, entry.responsePayloadMetadata, jsonFoldingEnabled)
}
}
}
Expand Down Expand Up @@ -343,7 +350,11 @@ private enum class PayloadViewMode {
}

@Composable
private fun DataTab(data: String?, metadata: MessagePayloadMetadata?) {
private fun DataTab(
data: String?,
metadata: MessagePayloadMetadata?,
jsonFoldingEnabled: Boolean,
) {
val rawPreview = metadata?.rawPreview
var viewMode by remember(rawPreview?.base64) { mutableStateOf(PayloadViewMode.DECODED) }

Expand Down Expand Up @@ -372,18 +383,20 @@ private fun DataTab(data: String?, metadata: MessagePayloadMetadata?) {
if (viewMode == PayloadViewMode.RAW && rawPreview != null) {
RawPayloadView(metadata)
} else {
DecodedPayloadView(data)
DecodedPayloadView(data, jsonFoldingEnabled)
}
}
}
}

@Composable
private fun DecodedPayloadView(data: String?) {
Column(
private fun DecodedPayloadView(
data: String?,
jsonFoldingEnabled: Boolean,
) {
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
) {
if (data.isNullOrBlank()) {
Expand All @@ -393,7 +406,11 @@ private fun DecodedPayloadView(data: String?) {
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
JsonViewer(jsonString = data)
JsonViewer(
jsonString = data,
foldingEnabled = jsonFoldingEnabled,
modifier = Modifier.fillMaxSize(),
)
}
}
}
Expand Down
Loading