@@ -23,13 +23,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
2323import androidx.compose.material3.HorizontalDivider
2424import androidx.compose.material3.Icon
2525import androidx.compose.material3.IconButton
26+ import androidx.compose.material3.MaterialTheme
2627import androidx.compose.material3.Surface
2728import androidx.compose.material3.Text
2829import androidx.compose.material3.TextField
2930import androidx.compose.material3.TextFieldDefaults
31+ import androidx.compose.material3.darkColorScheme
3032import androidx.compose.runtime.Composable
3133import androidx.compose.runtime.collectAsState
3234import androidx.compose.runtime.getValue
35+ import androidx.compose.runtime.remember
3336import androidx.compose.ui.Alignment
3437import androidx.compose.ui.Modifier
3538import androidx.compose.ui.graphics.Color
@@ -50,7 +53,6 @@ import androidx.compose.ui.unit.dp
5053import androidx.compose.ui.unit.sp
5154import androidx.lifecycle.ViewModel
5255import androidx.lifecycle.viewModelScope
53- import jisho.ui.theme.Theme
5456import kotlinx.coroutines.Job
5557import kotlinx.coroutines.flow.MutableStateFlow
5658import kotlinx.coroutines.flow.StateFlow
@@ -63,19 +65,24 @@ class MainActivity : ComponentActivity() {
6365 super .onCreate(savedInstanceState)
6466 val searchModel: SearchModel by viewModels()
6567 setContent {
66- Theme {
68+ MaterialTheme (
69+ colorScheme = darkColorScheme()
70+ ) {
6771 Surface {
68- Column (
69- Modifier
72+ val results by searchModel.results.collectAsState(emptyList())
73+ LazyColumn (
74+ modifier = Modifier
7075 .fillMaxSize()
71- .padding(WindowInsets .systemBars.asPaddingValues()), // @note on screen camera
72- horizontalAlignment = Alignment .CenterHorizontally
76+ .padding(WindowInsets .systemBars.asPaddingValues())
7377 ) {
74- SearchBar (
75- searchModel
76- ) { searchModel.search(it) }
77- val results by searchModel.results.collectAsState(emptyList())
78- if (results.isNotEmpty()) Results (results, searchModel)
78+ item {
79+ SearchBar (
80+ searchModel
81+ ) { searchModel.search(it) }
82+ }
83+ items(results) { jishoData ->
84+ ItemColumn (jishoData, searchModel)
85+ }
7986 }
8087 }
8188 }
@@ -91,8 +98,8 @@ class MainActivity : ComponentActivity() {
9198 val iPos by searchModel.indicatorPos.collectAsState(0 )
9299 TextField (
93100 value = TextFieldValue (text = search, selection = TextRange (iPos)),
94- onValueChange = { newValue ->
95- onValueChange(newValue .text)
101+ onValueChange = {
102+ onValueChange(it .text)
96103 },
97104 modifier = Modifier
98105 .fillMaxWidth()
@@ -138,122 +145,90 @@ class MainActivity : ComponentActivity() {
138145 }
139146
140147 @Composable
141- fun Results (results : List <JishoData >, searchModel : SearchModel ) {
142- val search by searchModel.search.collectAsState()
143- LazyColumn (
144- modifier = Modifier
145- .fillMaxSize()
146- .padding(8 .dp)
148+ fun ItemColumn (jishoData : JishoData , searchModel : SearchModel ) {
149+ Column (
150+ horizontalAlignment = Alignment .CenterHorizontally
147151 ) {
148- item {
149- if (search.all { it in ' a' .. ' z' || it in ' A' .. ' Z' } &&
150- search.canEtoH()) {
151- val annotatedString = buildAnnotatedString {
152- append(" Searched for " )
153- withStyle(SpanStyle (fontWeight = FontWeight .Bold )) {
154- append(" ${replaceEtoH(search)} ." )
155- }
156- append(" You can also try a search for " )
157- pushStringAnnotation(
158- tag = " clickSearch" ,
159- annotation = " \" $search \" "
160- )
161- withStyle(SpanStyle (textDecoration = TextDecoration .Underline )) {
162- append(" \" $search \" " )
163- }
164- pop()
165- append(" ." )
166- }
167- var textLayoutResult: TextLayoutResult ? = null
168- Text (
169- text = annotatedString,
170- modifier = Modifier
171- .pointerInput(annotatedString) {
172- tapGesture(searchModel, annotatedString, textLayoutResult)
173- }
174- .padding(bottom = 18 .dp),
175- onTextLayout = { textLayoutResult = it },
176- )
177- }
152+ var japanese = jishoData.japanese.firstOrNull()
153+ Text (
154+ text = japanese?.let { if (it.word != null ) it.reading else " " }.orEmpty(),
155+ fontSize = 16 .sp
156+ )
157+ Text (
158+ text = japanese?.let { it.word ? : it.reading }.orEmpty(),
159+ fontSize = 32 .sp
160+ )
161+ }
162+ Row (
163+ horizontalArrangement = Arrangement .spacedBy(6 .dp)
164+ ) {
165+ if (jishoData.isCommon) {
166+ wordTag(" common word" , Color (0xFF8abc83 ))
178167 }
179- items(results) { word ->
180- Column (
181- horizontalAlignment = Alignment .CenterHorizontally
182- ) {
183- var japanese = word.japanese.firstOrNull()
184- Text (
185- text = japanese?.let { if (it.word != null ) it.reading else " " }.orEmpty(),
186- fontSize = 16 .sp
187- )
188- Text (
189- text = japanese?.let { it.word ? : it.reading }.orEmpty(),
190- fontSize = 32 .sp
191- )
192- }
193- Row (
194- horizontalArrangement = Arrangement .spacedBy(6 .dp)
195- ) {
196- if (word.isCommon) {
197- wordTag(" common word" , Color (0xFF8abc83 ))
168+ if (jishoData.jlpt.isNotEmpty()) {
169+ wordTag(
170+ jishoData.jlpt.firstOrNull().orEmpty().replace(" -" , " " ),
171+ Color (0xFF909dc0 )
172+ )
173+ }
174+ if (jishoData.tags.isNotEmpty()) {
175+ wordTag(
176+ " Wanikani level ${jishoData.tags.firstOrNull()?.lastOrNull().toString()} " ,
177+ Color (0xFF909dc0 )
178+ )
179+ }
180+ }
181+ for ((index, sense) in jishoData.senses.withIndex()) {
182+ val annotatedString = remember(sense, index) {
183+ buildAnnotatedString {
184+ withStyle(
185+ SpanStyle (
186+ color = Color (0xFFAAAAAA ),
187+ fontSize = 16 .sp,
188+ fontWeight = FontWeight .Bold
189+ )
190+ ) {
191+ append(" ${sense.partsOfSpeech.joinToString(" , " ) { it }} \n " )
198192 }
199- if (word.jlpt.isNotEmpty( )) {
200- wordTag(word.jlpt.firstOrNull().orEmpty(), Color ( 0xFF909dc0 ) )
193+ withStyle( SpanStyle (color = Color ( 0xFF999999 ), fontSize = 18 .sp )) {
194+ append( " ${index + 1 } . " )
201195 }
202- if (word.tags.isNotEmpty()) {
203- wordTag(word.tags.firstOrNull().orEmpty(), Color (0xFF909dc0 ))
196+ sense.englishDefinitions.forEachIndexed { index, definition ->
197+ append(definition)
198+ if (index != sense.englishDefinitions.lastIndex) append(" ; " )
204199 }
205- }
206- for ((index, sense) in word.senses.withIndex()) {
207- val annotatedString = buildAnnotatedString {
208- withStyle(
209- SpanStyle (
210- color = Color (0xFFAAAAAA ),
211- fontSize = 16 .sp,
212- fontWeight = FontWeight .Bold
200+ withStyle(SpanStyle (color = Color (0xFFBBBBBB ), fontSize = 16 .sp)) {
201+ if (sense.seeAlso.isNotEmpty()) {
202+ append(" See also " )
203+ pushStringAnnotation(
204+ tag = " clickSearch" ,
205+ annotation = sense.seeAlso.firstOrNull().orEmpty()
213206 )
214- ) {
215- append(" ${sense.partsOfSpeech.joinToString(" , " ) { it }} \n " )
216- }
217- withStyle(SpanStyle (color = Color (0xFF999999 ), fontSize = 18 .sp)) {
218- append(" ${index + 1 } . " )
219- }
220- sense.englishDefinitions.forEachIndexed { index, definition ->
221- append(definition)
222- if (index != sense.englishDefinitions.lastIndex) append(" ; " )
223- }
224- withStyle(SpanStyle (color = Color (0xFFBBBBBB ), fontSize = 16 .sp)) {
225- if (sense.seeAlso.isNotEmpty()) {
226- append(" See also " )
227- pushStringAnnotation(
228- tag = " clickSearch" ,
229- annotation = sense.seeAlso.firstOrNull().orEmpty()
230- )
231- withStyle(SpanStyle (textDecoration = TextDecoration .Underline )) {
232- append(sense.seeAlso.firstOrNull().orEmpty())
233- }
234- pop()
235- }
236- if (sense.info.isNotEmpty()) {
237- if (sense.seeAlso.isNotEmpty()) append(' ,' )
238- append(" ${sense.info.firstOrNull().orEmpty()} " )
207+ withStyle(SpanStyle (textDecoration = TextDecoration .Underline )) {
208+ append(sense.seeAlso.firstOrNull().orEmpty())
239209 }
210+ pop()
211+ }
212+ if (sense.info.isNotEmpty()) {
213+ if (sense.seeAlso.isNotEmpty()) append(' ,' )
214+ append(" ${sense.info.firstOrNull().orEmpty()} " )
240215 }
241216 }
242- var textLayoutResult: TextLayoutResult ? = null
243- Text (
244- text = annotatedString,
245- modifier = Modifier
246- .pointerInput(annotatedString) {
247- tapGesture(searchModel, annotatedString, textLayoutResult)
248- }
249- .padding(10 .dp),
250- onTextLayout = { textLayoutResult = it },
251- style = TextStyle (fontSize = 20 .sp)
252- )
253217 }
254- HorizontalDivider ()
255218 }
219+ var textLayoutResult: TextLayoutResult ? = null
220+ Text (
221+ text = annotatedString,
222+ modifier = Modifier
223+ .pointerInput(Unit ) {
224+ tapGesture(searchModel, annotatedString, textLayoutResult)
225+ }
226+ .padding(10 .dp),
227+ onTextLayout = { textLayoutResult = it },
228+ style = TextStyle (fontSize = 20 .sp)
229+ )
256230 }
231+ HorizontalDivider ()
257232 }
258233
259234 @Composable
@@ -301,19 +276,25 @@ class MainActivity : ComponentActivity() {
301276
302277 class SearchModel : ViewModel () {
303278 private val _search = MutableStateFlow (" " )
304- val search: StateFlow <String > get() = _search
279+ val search: StateFlow <String > = _search
305280
306281 private val _results = MutableStateFlow <List <JishoData >>(emptyList())
307- val results: StateFlow <List <JishoData >> get() = _results
282+ val results: StateFlow <List <JishoData >> = _results
308283
309284 private var job: Job ? = null
310285 fun search (query : String , page : Int = 1) {
311286 _search .value = query
312287 _indicatorPos .value = query.length
313288 job?.takeIf { it.isActive }?.cancel()
289+ if (query.isEmpty()) {
290+ _results .value = emptyList()
291+ return
292+ }
314293 job = viewModelScope.launch {
315294 try {
316- search(query, page, { word ->
295+ val thisQuery = _search .value
296+ search(thisQuery, page, { word ->
297+ if (thisQuery != _search .value) throw CancellationException ()
317298 _results .value = word.data
318299 })
319300 } catch (_: CancellationException ) {
@@ -323,7 +304,7 @@ class MainActivity : ComponentActivity() {
323304 }
324305
325306 private val _indicatorPos = MutableStateFlow (0 )
326- val indicatorPos: StateFlow <Int > get() = _indicatorPos
307+ val indicatorPos: StateFlow <Int > = _indicatorPos
327308 fun updateIndicator (pos : Int ) {
328309 _indicatorPos .value = pos
329310 }
0 commit comments