Skip to content

Commit a6667f8

Browse files
committed
Added first research thing on duckduck go, doesn't work
1 parent 38f7ef9 commit a6667f8

6 files changed

Lines changed: 183 additions & 3 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
1313
android:maxSdkVersion="32"/>
1414

15+
<uses-permission android:name="android.permission.INTERNET" />
16+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
17+
1518

1619
<application
1720
android:allowBackup="true"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.openinsectid.app.data
2+
3+
import android.net.Uri
4+
5+
data class DdgImage(
6+
val image: String,
7+
val thumbnail: String,
8+
val title: String?
9+
)
10+
11+
12+
suspend fun searchDuckDuckGoImages(query: String): List<DdgImage> {
13+
val url =
14+
"https://duckduckgo.com/i.js?q=${Uri.encode(query)}&o=json"
15+
16+
val connection = java.net.URL(url).openConnection() as java.net.HttpURLConnection
17+
connection.setRequestProperty("User-Agent", "Mozilla/5.0")
18+
connection.connectTimeout = 10_000
19+
connection.readTimeout = 10_000
20+
21+
connection.inputStream.use { stream ->
22+
val text = stream.bufferedReader().readText()
23+
val json = org.json.JSONObject(text)
24+
val results = json.getJSONArray("results")
25+
26+
return List(results.length()) { i ->
27+
val o = results.getJSONObject(i)
28+
DdgImage(
29+
image = o.getString("image"),
30+
thumbnail = o.getString("thumbnail"),
31+
title = o.optString("title")
32+
)
33+
}
34+
}
35+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.openinsectid.app.data
2+
3+
4+
sealed interface ImageSearchState {
5+
object Idle : ImageSearchState
6+
object Loading : ImageSearchState
7+
data class Success(val images: List<DdgImage>) : ImageSearchState
8+
data class Error(val message: String) : ImageSearchState
9+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package org.openinsectid.app.ui.components
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.fillMaxWidth
5+
import androidx.compose.foundation.layout.height
6+
import androidx.compose.material3.CircularProgressIndicator
7+
import androidx.compose.material3.MaterialTheme
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.LaunchedEffect
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.rememberCoroutineScope
15+
import androidx.compose.runtime.setValue
16+
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.unit.dp
19+
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.withContext
21+
import org.openinsectid.app.data.DdgImage
22+
import org.openinsectid.app.data.ImageSearchState
23+
import org.openinsectid.app.data.searchDuckDuckGoImages
24+
25+
@Composable
26+
fun DuckDuckGoImageSearch(
27+
query: String,
28+
onImageSelected: (DdgImage) -> Unit
29+
) {
30+
var state by remember { mutableStateOf<ImageSearchState>(ImageSearchState.Idle) }
31+
val scope = rememberCoroutineScope()
32+
33+
LaunchedEffect(query) {
34+
if (query.isBlank()) return@LaunchedEffect
35+
36+
state = ImageSearchState.Loading
37+
38+
try {
39+
val images = withContext(Dispatchers.IO) {
40+
searchDuckDuckGoImages(query)
41+
}
42+
state = ImageSearchState.Success(images)
43+
} catch (e: Exception) {
44+
state = ImageSearchState.Error("No internet connection")
45+
}
46+
}
47+
48+
when (state) {
49+
ImageSearchState.Idle -> Unit
50+
51+
ImageSearchState.Loading -> {
52+
Box(
53+
modifier = Modifier
54+
.fillMaxWidth()
55+
.height(200.dp),
56+
contentAlignment = Alignment.Center
57+
) {
58+
CircularProgressIndicator()
59+
}
60+
}
61+
62+
is ImageSearchState.Error -> {
63+
Text(
64+
text = (state as ImageSearchState.Error).message,
65+
color = MaterialTheme.colorScheme.error
66+
)
67+
}
68+
69+
is ImageSearchState.Success -> {
70+
val images = (state as ImageSearchState.Success).images
71+
if (images.isEmpty()) {
72+
Text("No images found")
73+
} else {
74+
ImageResultsGrid(images, onImageSelected)
75+
}
76+
}
77+
}
78+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.openinsectid.app.ui.components
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.PaddingValues
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.height
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.lazy.grid.items
9+
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.draw.clip
13+
import androidx.compose.ui.unit.dp
14+
import coil3.compose.AsyncImage
15+
import org.openinsectid.app.data.DdgImage
16+
17+
@Composable
18+
fun ImageResultsGrid(
19+
images: List<DdgImage>,
20+
onImageClick: (DdgImage) -> Unit
21+
) {
22+
androidx.compose.foundation.lazy.grid.LazyVerticalGrid(
23+
columns = androidx.compose.foundation.lazy.grid.GridCells.Fixed(2),
24+
modifier = Modifier.fillMaxWidth(),
25+
contentPadding = PaddingValues(8.dp)
26+
) {
27+
items(images) { img ->
28+
AsyncImage(
29+
model = img.thumbnail,
30+
contentDescription = img.title,
31+
modifier = Modifier
32+
.padding(6.dp)
33+
.fillMaxWidth()
34+
.height(160.dp)
35+
.clip(MaterialTheme.shapes.medium)
36+
.clickable { onImageClick(img) }
37+
)
38+
}
39+
}
40+
}

app/src/main/java/org/openinsectid/app/ui/screens/MainScreen.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import kotlinx.coroutines.Dispatchers
5858
import kotlinx.coroutines.launch
5959
import kotlinx.coroutines.withContext
6060
import org.openinsectid.app.data.ImageStore
61+
import org.openinsectid.app.ui.components.DuckDuckGoImageSearch
6162
import org.openinsectid.app.utils.InferenceManager
6263

6364

@@ -247,18 +248,32 @@ fun MainScreen(navController: NavController) {
247248
)
248249
}
249250

250-
predictions?.takeIf { it.isNotEmpty() }?.let { preds ->
251+
predictions?.takeIf { it.isNotEmpty() }?.let { infos ->
251252
Spacer(Modifier.height(16.dp))
252253
Text("Predictions:", style = MaterialTheme.typography.headlineSmall)
253-
preds.forEach { (level, prediction) ->
254+
infos.forEach { (level, prediction) ->
254255
Text(
255256
text = "$level: $prediction",
256257
style = MaterialTheme.typography.bodyLarge,
257258
color = MaterialTheme.colorScheme.onBackground,
258259
fontWeight = FontWeight.Bold
259260
)
260261
}
261-
} ?: predictions?.takeIf { it.isEmpty() }?.let {
262+
263+
val insectName = remember(infos) {
264+
infos
265+
.toSortedMap()
266+
.values
267+
.joinToString(" ")
268+
}
269+
DuckDuckGoImageSearch(
270+
query = insectName,
271+
onImageSelected = {
272+
// Nothing for now, but will open search
273+
}
274+
)
275+
276+
} ?: predictions?.let {
262277
Spacer(Modifier.height(16.dp))
263278
Text("No predictions found", color = Color.Red)
264279
}

0 commit comments

Comments
 (0)