Skip to content

Commit 8105164

Browse files
committed
Integrated Na'vi card display into search. The app now has basic functionality! Maybe I'll change a few cosmetic details later.
Next step: Audio and image.
1 parent 9d923bf commit 8105164

File tree

3 files changed

+181
-27
lines changed

3 files changed

+181
-27
lines changed

app/src/main/java/com/kip/reykunyu/ui/NaviCard.kt

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class)
1+
@file:OptIn(ExperimentalMaterial3Api::class)
22

33
package com.kip.reykunyu.ui
44

55
import android.net.Uri
6+
import android.util.Log
67
import android.util.Patterns
78
import androidx.browser.customtabs.CustomTabsIntent
89
import androidx.compose.animation.AnimatedContent
@@ -40,7 +41,7 @@ import com.kip.reykunyu.ui.theme.Typography
4041
ExperimentalAnimationApi::class
4142
)
4243
@Composable
43-
fun NaviCard(navi: Navi) {
44+
fun NaviCard(navi: Navi, naviClick: (String) -> Unit) {
4445
val labelLarge = MaterialTheme.typography.labelLarge.copy(fontSize = 17.sp)
4546
ElevatedCard(
4647
modifier = Modifier
@@ -53,7 +54,7 @@ fun NaviCard(navi: Navi) {
5354
verticalAlignment = Alignment.CenterVertically,
5455
modifier = Modifier.padding(horizontal = 17.dp, vertical = 7.dp)
5556
) {
56-
//Navi
57+
//Na'vi
5758
Text(
5859
text = navi.word,
5960
style = Typography.titleLarge
@@ -77,12 +78,14 @@ fun NaviCard(navi: Navi) {
7778
text = stringResource(id = navi.typeDetails()),
7879
maxLines = 2,
7980
overflow = TextOverflow.Ellipsis,
80-
style = MaterialTheme.typography.titleSmall
81+
style = MaterialTheme.typography.displaySmall
82+
.copy(fontSize = 20.sp)
8183
)
8284
}else {
8385
Text(
8486
text = navi.typeDisplay(),
85-
style = MaterialTheme.typography.titleMedium
87+
style = MaterialTheme.typography.displaySmall
88+
.copy(fontSize = 20.sp)
8689
)
8790
}
8891
}
@@ -174,14 +177,14 @@ fun NaviCard(navi: Navi) {
174177

175178
//meaning note
176179
if (navi.meaning_note != null) {
177-
RichText(content = navi.meaning_note, naviClick = {/*TODO*/})
180+
RichText(content = navi.meaning_note, naviClick = naviClick)
178181
}
179182

180183
AutoSpacer(navi.translations, navi.meaning_note, 5.dp, divider = false)
181184

182185

183186
//etymology
184-
InfoModule(category = "ETYMOLOGY", content = navi.etymology)
187+
InfoModule(category = "ETYMOLOGY", content = navi.etymology, naviClick = naviClick)
185188

186189
//See also
187190
if (navi.seeAlso != null) {
@@ -196,7 +199,7 @@ fun NaviCard(navi: Navi) {
196199
Modifier.padding(horizontal = 20.dp)
197200
) {
198201
for (refNavi in navi.seeAlso) {
199-
NaviReferenceChip(naviUnformatted = refNavi, onClick = {/*TODO*/},
202+
NaviReferenceChip(naviUnformatted = refNavi, onClick = naviClick,
200203
paddingR = 10.dp)
201204
}
202205
}
@@ -227,15 +230,18 @@ fun NaviCard(navi: Navi) {
227230

228231
//status
229232
InfoModule(category = "STATUS", content = navi.status?.uppercase(),
230-
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Black))
233+
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Black),
234+
naviClick = naviClick
235+
)
231236

232237
//statusNote
233-
InfoModule(category = "NOTE", content = navi.status_note, padding = 3.dp)
238+
InfoModule(category = "NOTE", content = navi.status_note, padding = 3.dp,
239+
naviClick = naviClick)
234240

235241
AutoSpacer(navi.status, navi.status_note, padding = 3.dp, divider = false)
236242

237243
//Sources
238-
SourcesCard(sources = navi.source)
244+
SourcesCard(sources = navi.source, naviClick = naviClick)
239245

240246
Spacer(Modifier.padding(6.dp))
241247
}
@@ -251,7 +257,7 @@ fun InfoModule(
251257
content: String?,
252258
style: TextStyle = Typography.bodyLarge,
253259
padding: Dp = 8.dp,
254-
naviClick: (String) -> Unit = {}
260+
naviClick: (String) -> Unit
255261
) {
256262
val labelLarge = MaterialTheme.typography.labelLarge.copy(fontSize = 17.sp)
257263
if (content == null) {
@@ -320,7 +326,8 @@ fun RichText(
320326
ClickableText(
321327
text = component.url!!,
322328
style = style.copy(
323-
textDecoration = TextDecoration.Underline
329+
textDecoration = TextDecoration.Underline,
330+
color = MaterialTheme.colorScheme.onPrimary
324331
),
325332
onClick = {
326333
val builder = CustomTabsIntent.Builder()
@@ -371,7 +378,10 @@ fun NaviReferenceChip(
371378
.padding(horizontal = paddingL)
372379
.defaultMinSize(minHeight = 0.dp))
373380
AssistChip(
374-
onClick = { onClick(refNavi) },
381+
onClick = {
382+
Log.i("REYKUNYU", "Na'vi card clicked! $refNavi")
383+
onClick(refNavi)
384+
},
375385
label = {
376386
Text(
377387
text = refNavi,
@@ -469,10 +479,12 @@ data class RichTextComponent(
469479

470480
//endregion
471481

482+
@OptIn(ExperimentalMaterial3Api::class)
472483
@Composable
473484
fun SourcesCard(
474485
sources: List<List<String>>?,
475-
style: TextStyle = Typography.bodyLarge
486+
style: TextStyle = Typography.bodyLarge,
487+
naviClick: (String) -> Unit
476488
) {
477489
if (sources.isNullOrEmpty()) {
478490
return
@@ -563,7 +575,7 @@ fun SourcesCard(
563575
for (entry in source) {
564576
RichText(
565577
content = entry,
566-
naviClick = {/* TODO */ },
578+
naviClick = naviClick,
567579
padding = false
568580
)
569581
}
@@ -654,7 +666,7 @@ fun NaviCardPreview() {
654666
)
655667
LazyColumn {
656668
items(items = naviList) { item ->
657-
NaviCard(item)
669+
NaviCard(item, {})
658670
}
659671
}
660672
}

app/src/main/java/com/kip/reykunyu/ui/screens/DictionaryScreen.kt

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,55 @@
22

33
package com.kip.reykunyu.ui.screens
44

5+
import android.util.Log
6+
import androidx.compose.animation.AnimatedContent
7+
import androidx.compose.animation.ExperimentalAnimationApi
8+
import androidx.compose.animation.core.animateFloatAsState
9+
import androidx.compose.animation.core.tween
10+
import androidx.compose.foundation.ExperimentalFoundationApi
511
import androidx.compose.foundation.layout.*
12+
import androidx.compose.foundation.lazy.LazyColumn
13+
import androidx.compose.foundation.lazy.LazyListState
14+
import androidx.compose.foundation.lazy.items
15+
import androidx.compose.foundation.lazy.rememberLazyListState
16+
import androidx.compose.foundation.pager.HorizontalPager
17+
import androidx.compose.foundation.pager.rememberPagerState
618
import androidx.compose.foundation.text.KeyboardActions
719
import androidx.compose.foundation.text.KeyboardOptions
820
import androidx.compose.material.icons.Icons
921
import androidx.compose.material.icons.filled.Menu
1022
import androidx.compose.material.icons.filled.Search
23+
import androidx.compose.material.icons.rounded.Search
1124
import androidx.compose.material3.*
1225
import androidx.compose.runtime.*
1326
import androidx.compose.ui.Alignment
1427
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.composed
29+
import androidx.compose.ui.draw.drawWithContent
30+
import androidx.compose.ui.geometry.Offset
31+
import androidx.compose.ui.geometry.Size
1532
import androidx.compose.ui.platform.LocalFocusManager
1633
import androidx.compose.ui.res.stringResource
1734
import androidx.compose.ui.text.input.ImeAction
35+
import androidx.compose.ui.text.style.TextAlign
36+
import androidx.compose.ui.text.style.TextOverflow
1837
import androidx.compose.ui.tooling.preview.Preview
38+
import androidx.compose.ui.unit.Dp
1939
import androidx.compose.ui.unit.dp
2040
import androidx.compose.ui.unit.sp
2141
import androidx.lifecycle.viewmodel.compose.viewModel
2242
import com.kip.reykunyu.R
43+
import com.kip.reykunyu.data.dict.Navi
44+
import com.kip.reykunyu.ui.NaviCard
2345
import com.kip.reykunyu.viewmodels.DictSearchState
2446
import com.kip.reykunyu.viewmodels.DictionarySearchViewModel
2547
import com.kip.reykunyu.viewmodels.OfflineDictState
2648
import com.kip.reykunyu.viewmodels.OfflineDictionaryViewModel
49+
import kotlinx.coroutines.launch
2750

2851

2952
//The class for the main dictionary UI
30-
@OptIn(ExperimentalMaterial3Api::class)
53+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
3154
@Preview
3255
@Composable
3356
fun DictionaryScreen(
@@ -86,15 +109,26 @@ fun DictionaryScreen(
86109
LoadingView()
87110
}
88111
is OfflineDictState.Loaded -> {
89-
90-
when (val searchState = searchViewModel.dictSearchState) {
91-
is DictSearchState.Success -> {
92-
StatusText(text = searchState.result.toNavi.size.toString())
112+
AnimatedContent(targetState = searchViewModel.dictSearchState)
113+
{ state ->
114+
when (state) {
115+
is DictSearchState.Success -> {
116+
SearchDisplay(
117+
fromNavi = state.result.fromNavi,
118+
toNavi = state.result.toNavi,
119+
naviAction = {
120+
Log.i("REYKUNYU", "NAVI REF: $it")
121+
searchViewModel.updateSearchInput(it)
122+
onSearch()
123+
}
124+
)
125+
}
126+
DictSearchState.Error -> StatusText(text = "ERROR!")
127+
DictSearchState.Loading -> LoadingView()
128+
DictSearchState.Standby -> StandbyHelpView()
93129
}
94-
DictSearchState.Error -> StatusText(text = "ERROR!")
95-
DictSearchState.Loading -> StatusText(text = "LOADING...")
96-
DictSearchState.Standby -> {}
97130
}
131+
98132
}
99133
is OfflineDictState.Error -> {
100134
StatusText(text = "ERROR!")
@@ -138,6 +172,28 @@ fun LoadingView() {
138172
}
139173
}
140174

175+
@Composable
176+
fun StandbyHelpView() {
177+
Column(
178+
Modifier.fillMaxSize(),
179+
horizontalAlignment = Alignment.CenterHorizontally,
180+
verticalArrangement = Arrangement.Center
181+
) {
182+
Icon(Icons.Rounded.Search, null,
183+
modifier = Modifier
184+
.size(200.dp)
185+
.padding(0.dp)
186+
)
187+
Text(
188+
text = stringResource(R.string.string_help),
189+
style = MaterialTheme.typography.titleMedium,
190+
textAlign = TextAlign.Center,
191+
modifier = Modifier.padding(horizontal = 40.dp, vertical = 5.dp)
192+
)
193+
Spacer(Modifier.padding(60.dp))
194+
}
195+
}
196+
141197

142198
@Composable
143199
fun StatusText(
@@ -173,13 +229,98 @@ fun DictionarySearchBar(
173229
{
174230
Text(
175231
text = stringResource(R.string.search),
176-
style = MaterialTheme.typography.titleMedium
232+
style = MaterialTheme.typography.titleMedium.copy(fontSize = 20.sp),
177233
)
178234
},
179-
textStyle = MaterialTheme.typography.titleMedium,
235+
textStyle = MaterialTheme.typography.titleMedium.copy(fontSize = 20.sp),
180236
singleLine = true,
181237
modifier = modifier
182238
.fillMaxWidth()
183239
.heightIn(32.dp)
184240
)
185241
}
242+
243+
@OptIn(ExperimentalFoundationApi::class)
244+
@Composable
245+
fun SearchDisplay(fromNavi: List<Navi>, toNavi: List<Navi>, naviAction: (String) -> Unit) {
246+
val initPage = if (fromNavi.isEmpty() && toNavi.isNotEmpty()) { 1 } else { 0 }
247+
248+
val state = rememberPagerState(initialPage = initPage)
249+
val titles = listOf("Na\'vi to Lang (${fromNavi.size})", "Lang to Na\'vi (${toNavi.size})")
250+
251+
val coroutineScope = rememberCoroutineScope()
252+
253+
Column {
254+
TabRow(selectedTabIndex = state.currentPage) {
255+
titles.forEachIndexed { index, title ->
256+
Tab(
257+
selected = state.currentPage == index,
258+
onClick = {
259+
coroutineScope.launch {
260+
state.animateScrollToPage(index)
261+
}
262+
},
263+
text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
264+
)
265+
}
266+
}
267+
268+
HorizontalPager(pageCount = 2, state = state) {
269+
when (state.currentPage) {
270+
0 -> NaviList(naviList = fromNavi, naviAction = { naviAction(it) })
271+
1 -> NaviList(naviList = toNavi, naviAction = { naviAction(it) })
272+
}
273+
}
274+
}
275+
}
276+
277+
@Composable
278+
fun NaviList(naviList: List<Navi>, naviAction: (String) -> Unit) {
279+
val state: LazyListState = rememberLazyListState()
280+
LazyColumn(
281+
state = state,
282+
modifier = Modifier.simpleVerticalScrollbar(state)
283+
) {
284+
items(naviList) {item ->
285+
NaviCard(navi = item, naviClick = { naviAction(it) })
286+
}
287+
}
288+
}
289+
290+
291+
292+
fun Modifier.simpleVerticalScrollbar(
293+
state: LazyListState,
294+
width: Dp = 8.dp
295+
): Modifier = composed {
296+
val targetAlpha = if (state.isScrollInProgress) 1f else 0f
297+
val duration = if (state.isScrollInProgress) 150 else 500
298+
299+
val alpha by animateFloatAsState(
300+
targetValue = targetAlpha,
301+
animationSpec = tween(durationMillis = duration)
302+
)
303+
304+
val color = MaterialTheme.colorScheme.tertiary
305+
306+
drawWithContent {
307+
drawContent()
308+
309+
val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index
310+
val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f
311+
312+
// Draw scrollbar if scrolling or if the animation is still running and lazy column has content
313+
if (needDrawScrollbar && firstVisibleElementIndex != null) {
314+
val elementHeight = this.size.height / state.layoutInfo.totalItemsCount
315+
val scrollbarOffsetY = firstVisibleElementIndex * elementHeight
316+
val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight
317+
318+
drawRect(
319+
color = color,
320+
topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY),
321+
size = Size(width.toPx(), scrollbarHeight),
322+
alpha = alpha
323+
)
324+
}
325+
}
326+
}

app/src/main/res/values/strings_ui.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<!-- UI -->
1818
<string name="search">Search..</string>
19+
<string name="string_help">Type Na\'vi or your preferred language in the search bar to search</string>
1920

2021

2122
</resources>

0 commit comments

Comments
 (0)