diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..8ab0cbb --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,11 @@ +# Palette's Journal - FREE-AI-ROCK + +This journal tracks critical UX/accessibility learnings specific to this application's components and design system. + +## 2025-02-14 - Auto-Extraction from Raw URLs on Paste +**Learning:** Users naturally copy the entire browser URL (like a full PR link or a raw code link) rather than manually extracting the owner, repo name, PR number, or filename. Requiring them to manually parse these details is a significant cognitive load and causes friction. +**Action:** Always intercepts text input in repo/PR fields. If a URL format is detected, automatically extract all relevant segments (owner, repo, filename, PR #) and distribute them to the corresponding form fields in a single operation. + +## 2025-02-14 - Aesthetic-Consistent Show/Hide Secret Toggles +**Learning:** This app operates under a super-minimalist aesthetic and has zero vector icon dependencies (the bottom navigation uses monospace characters as icons). Adding standard vector icons for password toggle (visibility/visibility-off) would introduce dependency overhead or visual inconsistency. +**Action:** Design custom text-based toggles (e.g., "Show" / "Hide" monospace button) as `trailingIcon` on password inputs. This keeps the design system beautifully uniform, maintains full screen-reader accessibility, and avoids bundling heavy vector drawables. diff --git a/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt b/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt index a592281..14c459a 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt @@ -28,7 +28,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider import com.sayanthrock.freeairock.data.ai.CodeAnalysisState @@ -157,6 +159,17 @@ class MainActivity : ComponentActivity() { } } +fun extractFileNameFromUrl(url: String): String? { + val cleanUrl = url.substringBefore('?').substringBefore('#').trimEnd('/') + if (cleanUrl.contains('/')) { + val lastSegment = cleanUrl.substringAfterLast('/') + if (lastSegment.isNotBlank() && lastSegment.contains('.')) { + return lastSegment + } + } + return null +} + @Composable private fun CodeAnalyzerScreen( uiState: CodeAnalysisState, @@ -166,6 +179,7 @@ private fun CodeAnalyzerScreen( modifier: Modifier = Modifier ) { var githubToken by remember { mutableStateOf("") } + var isTokenVisible by remember { mutableStateOf(false) } var fileName by remember { mutableStateOf("MainActivity.kt") } var fileUrl by remember { mutableStateOf("") } @@ -201,7 +215,18 @@ private fun CodeAnalyzerScreen( value = githubToken, onValueChange = { githubToken = it }, label = { Text("GitHub token") }, - visualTransformation = PasswordVisualTransformation(), + visualTransformation = if (isTokenVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + androidx.compose.material3.TextButton( + onClick = { isTokenVisible = !isTokenVisible } + ) { + Text( + text = if (isTokenVisible) "Hide" else "Show", + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.primary + ) + } + }, modifier = Modifier.fillMaxWidth(), singleLine = true ) @@ -250,7 +275,12 @@ private fun CodeAnalyzerScreen( OutlinedTextField( value = fileUrl, - onValueChange = { fileUrl = it }, + onValueChange = { url -> + fileUrl = url + extractFileNameFromUrl(url)?.let { + fileName = it + } + }, label = { Text("GitHub raw/blob file URL") }, modifier = Modifier.fillMaxWidth(), singleLine = true diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt index d63a862..0d113cc 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt @@ -85,9 +85,18 @@ fun ReviewScreen( OutlinedTextField( value = prNumberText, - onValueChange = { prNumberText = it }, - label = { Text("Pull Request #") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { input -> + val parsed = parseGitHubPrUrl(input) + if (parsed != null) { + ownerText = parsed.first + repoText = parsed.second + prNumberText = parsed.third + } else { + prNumberText = input + } + }, + label = { Text("Pull Request # or full URL") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), modifier = Modifier.fillMaxWidth(), singleLine = true ) @@ -155,3 +164,11 @@ fun ReviewScreen( } } } + +fun parseGitHubPrUrl(url: String): Triple? { + val cleanUrl = url.substringBefore('?').substringBefore('#').trim().trimEnd('/') + val regex = """^(?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+)(?:/.*)?$""".toRegex(RegexOption.IGNORE_CASE) + val matchResult = regex.matchEntire(cleanUrl) ?: return null + val (owner, repo, prNumber) = matchResult.destructured + return Triple(owner, repo, prNumber) +} diff --git a/app/src/test/java/com/sayanthrock/freeairock/ui/PrUrlParserTest.kt b/app/src/test/java/com/sayanthrock/freeairock/ui/PrUrlParserTest.kt new file mode 100644 index 0000000..498e043 --- /dev/null +++ b/app/src/test/java/com/sayanthrock/freeairock/ui/PrUrlParserTest.kt @@ -0,0 +1,69 @@ +package com.sayanthrock.freeairock.ui + +import com.sayanthrock.freeairock.extractFileNameFromUrl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class PrUrlParserTest { + + @Test + fun testParseGitHubPrUrl_validUrls() { + val testCases = listOf( + "https://github.com/SayanthRock/FREE-AI-ROCK-/pull/12" to Triple("SayanthRock", "FREE-AI-ROCK-", "12"), + "http://github.com/SayanthRock/Root-apk/pull/123" to Triple("SayanthRock", "Root-apk", "123"), + "github.com/SayanthRock/Root-apk/pull/123/" to Triple("SayanthRock", "Root-apk", "123"), + "https://www.github.com/SayanthRock/Root-apk/pull/123/files" to Triple("SayanthRock", "Root-apk", "123"), + "https://github.com/SayanthRock/Root-apk/pull/123?diff=unified" to Triple("SayanthRock", "Root-apk", "123") + ) + + for ((input, expected) in testCases) { + val result = parseGitHubPrUrl(input) + assertEquals("Failed on input: $input", expected, result) + } + } + + @Test + fun testParseGitHubPrUrl_invalidUrls() { + val testCases = listOf( + "https://github.com/SayanthRock/FREE-AI-ROCK-", + "not-a-url", + "https://github.com/SayanthRock/FREE-AI-ROCK-/issues/1", + "https://gitlab.com/SayanthRock/Root-apk/pull/123" + ) + + for (input in testCases) { + val result = parseGitHubPrUrl(input) + assertNull("Expected null on input: $input", result) + } + } + + @Test + fun testExtractFileNameFromUrl_validUrls() { + val testCases = listOf( + "https://github.com/SayanthRock/FREE-AI-ROCK-/blob/main/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt" to "MainActivity.kt", + "https://raw.githubusercontent.com/SayanthRock/FREE-AI-ROCK-/main/app/build.gradle.kts" to "build.gradle.kts", + "https://github.com/owner/repo/blob/branch/src/App.tsx?someQuery=1" to "App.tsx", + "github.com/owner/repo/blob/branch/src/index.html#anchor" to "index.html" + ) + + for ((input, expected) in testCases) { + val result = extractFileNameFromUrl(input) + assertEquals("Failed on input: $input", expected, result) + } + } + + @Test + fun testExtractFileNameFromUrl_invalidUrls() { + val testCases = listOf( + "https://github.com/SayanthRock/FREE-AI-ROCK-", + "https://github.com/SayanthRock/FREE-AI-ROCK-/tree/main/app", + "not-a-file-url" + ) + + for (input in testCases) { + val result = extractFileNameFromUrl(input) + assertNull("Expected null on input: $input", result) + } + } +}