~repos /only-bible-app

#kotlin#android#ios

git clone https://pyrossh.dev/repos/only-bible-app.git

The only bible app you will ever need. No ads. No in-app purchases. No distractions.


0ff65d03 Peter John

1 year ago
add searchbar
app/src/main/java/dev/pyrossh/onlyBible/AppDrawer.kt CHANGED
@@ -26,6 +26,7 @@ import androidx.compose.material3.ModalNavigationDrawer
26
26
  import androidx.compose.material3.Text
27
27
  import androidx.compose.material3.rememberDrawerState
28
28
  import androidx.compose.runtime.Composable
29
+ import androidx.compose.runtime.collectAsState
29
30
  import androidx.compose.runtime.getValue
30
31
  import androidx.compose.runtime.mutableIntStateOf
31
32
  import androidx.compose.runtime.mutableStateOf
@@ -78,6 +79,7 @@ fun AppDrawer(
78
79
  var menuType by rememberSaveable {
79
80
  mutableStateOf(MenuType.Chapter)
80
81
  }
82
+ val bookNames by model.bookNames.collectAsState()
81
83
  val openDrawer = { m: MenuType, b: Int ->
82
84
  menuType = m
83
85
  bookIndex = b
@@ -177,7 +179,7 @@ fun AppDrawer(
177
179
  }
178
180
  items(39) { b ->
179
181
  QuickButton(
180
- title = shortName(model.bookNames[b]),
182
+ title = shortName(bookNames[b]),
181
183
  subtitle = null,
182
184
  ) {
183
185
  bookIndex = b
@@ -200,7 +202,7 @@ fun AppDrawer(
200
202
  items(27) { i ->
201
203
  val b = 39 + i
202
204
  QuickButton(
203
- title = shortName(model.bookNames[b]),
205
+ title = shortName(bookNames[b]),
204
206
  subtitle = null,
205
207
  ) {
206
208
  bookIndex = b
@@ -224,7 +226,7 @@ fun AppDrawer(
224
226
  verticalAlignment = Alignment.CenterVertically,
225
227
  ) {
226
228
  Text(
227
- text = model.bookNames[bookIndex],
229
+ text = bookNames[bookIndex],
228
230
  fontSize = 20.sp,
229
231
  fontWeight = FontWeight.W500
230
232
  )
app/src/main/java/dev/pyrossh/onlyBible/AppHost.kt CHANGED
@@ -75,7 +75,7 @@ fun AppHost(model: AppViewModel = viewModel()) {
75
75
  if (model.isLoading) it.alpha(0.5f) else it
76
76
  }
77
77
  ) {
78
- if (model.verses.isNotEmpty()) {
78
+ if (model.verses.value.isNotEmpty()) {
79
79
  AppDrawer(model = model, navigateToChapter = navigateToChapter) { openDrawer ->
80
80
  NavHost(
81
81
  navController = navController,
app/src/main/java/dev/pyrossh/onlyBible/AppViewModel.kt CHANGED
@@ -27,9 +27,16 @@ import com.microsoft.cognitiveservices.speech.SpeechSynthesizer
27
27
  import dev.pyrossh.onlyBible.domain.Verse
28
28
  import kotlinx.coroutines.CoroutineScope
29
29
  import kotlinx.coroutines.Dispatchers
30
+ import kotlinx.coroutines.FlowPreview
31
+ import kotlinx.coroutines.flow.MutableStateFlow
32
+ import kotlinx.coroutines.flow.SharingStarted
33
+ import kotlinx.coroutines.flow.asStateFlow
30
34
  import kotlinx.coroutines.flow.collectLatest
35
+ import kotlinx.coroutines.flow.combine
36
+ import kotlinx.coroutines.flow.debounce
31
37
  import kotlinx.coroutines.flow.distinctUntilChanged
32
38
  import kotlinx.coroutines.flow.map
39
+ import kotlinx.coroutines.flow.stateIn
33
40
  import kotlinx.coroutines.launch
34
41
  import kotlinx.coroutines.withContext
35
42
  import java.io.IOException
@@ -48,8 +55,8 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
48
55
  )
49
56
  var isLoading by mutableStateOf(true)
50
57
  var isOnError by mutableStateOf(false)
51
- var verses by mutableStateOf(listOf<Verse>())
58
+ val verses = MutableStateFlow(listOf<Verse>())
52
- var bookNames by mutableStateOf(listOf<String>())
59
+ val bookNames = MutableStateFlow(listOf<String>())
53
60
  var showBottomSheet by mutableStateOf(false)
54
61
  var bookIndex by preferenceMutableState(
55
62
  coroutineScope = viewModelScope,
@@ -97,6 +104,48 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
97
104
  getPreferencesKey = ::booleanPreferencesKey,
98
105
  )
99
106
 
107
+ private val _isSearching = MutableStateFlow(false)
108
+ val isSearching = _isSearching.asStateFlow()
109
+
110
+ //second state the text typed by the user
111
+ private val _searchText = MutableStateFlow("")
112
+ val searchText = _searchText.asStateFlow()
113
+
114
+ @OptIn(FlowPreview::class)
115
+ val versesList = _searchText.asStateFlow()
116
+ .debounce(300)
117
+ .combine(verses.asStateFlow()) { text, verses ->
118
+ verses.filter { verse ->
119
+ if (text.trim().isEmpty())
120
+ false
121
+ else
122
+ verse.text.lowercase().contains(
123
+ text.trim().lowercase()
124
+ )
125
+ }
126
+ }.stateIn(
127
+ scope = viewModelScope,
128
+ started = SharingStarted.WhileSubscribed(5000),
129
+ initialValue = listOf()
130
+ )
131
+
132
+ fun onSearchTextChange(text: String) {
133
+ _searchText.value = text
134
+ }
135
+
136
+ fun onOpenSearch() {
137
+ _isSearching.value = true
138
+ if (!_isSearching.value) {
139
+ onSearchTextChange("")
140
+ }
141
+ }
142
+
143
+ fun onCloseSearch() {
144
+ _isSearching.value = false
145
+ if (!_isSearching.value) {
146
+ onSearchTextChange("")
147
+ }
148
+ }
100
149
 
101
150
  fun showSheet() {
102
151
  showBottomSheet = true
@@ -126,7 +175,8 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
126
175
  }
127
176
  try {
128
177
  val buffer =
129
- context.assets.open("bibles/${loc.getDisplayLanguage(Locale.ENGLISH)}.txt").bufferedReader()
178
+ context.assets.open("bibles/${loc.getDisplayLanguage(Locale.ENGLISH)}.txt")
179
+ .bufferedReader()
130
180
  val localVerses = buffer.readLines().filter { it.isNotEmpty() }.map {
131
181
  val arr = it.split("|")
132
182
  val bookName = arr[0]
@@ -147,8 +197,8 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
147
197
  launch(Dispatchers.Main) {
148
198
  isLoading = false
149
199
  isOnError = false
150
- verses = localVerses
200
+ verses.value = localVerses
151
- bookNames = localVerses.distinctBy { it.bookName }.map { it.bookName }
201
+ bookNames.value = localVerses.distinctBy { it.bookName }.map { it.bookName }
152
202
  }
153
203
  } catch (e: IOException) {
154
204
  e.printStackTrace()
app/src/main/java/dev/pyrossh/onlyBible/ChapterScreen.kt CHANGED
@@ -1,6 +1,5 @@
1
1
  package dev.pyrossh.onlyBible
2
2
 
3
- import android.app.Activity
4
3
  import android.graphics.Typeface
5
4
  import android.os.Parcelable
6
5
  import android.text.Html
@@ -34,12 +33,16 @@ import androidx.compose.material.icons.outlined.MoreVert
34
33
  import androidx.compose.material.icons.outlined.PauseCircle
35
34
  import androidx.compose.material.icons.outlined.PlayCircle
36
35
  import androidx.compose.material.icons.outlined.Share
36
+ import androidx.compose.material.icons.rounded.Close
37
+ import androidx.compose.material.icons.rounded.Search
37
38
  import androidx.compose.material3.ExperimentalMaterial3Api
38
39
  import androidx.compose.material3.HorizontalDivider
39
40
  import androidx.compose.material3.Icon
40
41
  import androidx.compose.material3.IconButton
41
42
  import androidx.compose.material3.MaterialTheme
43
+ import androidx.compose.material3.ProvideTextStyle
42
44
  import androidx.compose.material3.Scaffold
45
+ import androidx.compose.material3.SearchBar
43
46
  import androidx.compose.material3.Surface
44
47
  import androidx.compose.material3.Text
45
48
  import androidx.compose.material3.TextButton
@@ -48,6 +51,7 @@ import androidx.compose.runtime.Composable
48
51
  import androidx.compose.runtime.DisposableEffect
49
52
  import androidx.compose.runtime.LaunchedEffect
50
53
  import androidx.compose.runtime.MutableIntState
54
+ import androidx.compose.runtime.collectAsState
51
55
  import androidx.compose.runtime.getValue
52
56
  import androidx.compose.runtime.mutableIntStateOf
53
57
  import androidx.compose.runtime.mutableStateOf
@@ -62,7 +66,6 @@ import androidx.compose.ui.input.pointer.PointerInputScope
62
66
  import androidx.compose.ui.input.pointer.pointerInput
63
67
  import androidx.compose.ui.platform.LocalContext
64
68
  import androidx.compose.ui.platform.LocalDensity
65
- import androidx.compose.ui.platform.LocalView
66
69
  import androidx.compose.ui.text.SpanStyle
67
70
  import androidx.compose.ui.text.TextStyle
68
71
  import androidx.compose.ui.text.buildAnnotatedString
@@ -137,6 +140,63 @@ suspend fun PointerInputScope.detectSwipe(
137
140
  }
138
141
  )
139
142
 
143
+ @Composable
144
+ @OptIn(ExperimentalMaterial3Api::class)
145
+ fun EmbeddedSearchBar(
146
+ query: String,
147
+ onQueryChange: (String) -> Unit,
148
+ onSearch: ((String) -> Unit),
149
+ onClose: () -> Unit,
150
+ content: @Composable () -> Unit
151
+ ) {
152
+ ProvideTextStyle(value = TextStyle(
153
+ fontSize = 18.sp,
154
+ color = MaterialTheme.colorScheme.onSurface,
155
+ )) {
156
+ SearchBar(
157
+ query = query,
158
+ onQueryChange = onQueryChange,
159
+ onSearch = onSearch,
160
+ active = true,
161
+ onActiveChange = {},
162
+ modifier = Modifier
163
+ .fillMaxWidth()
164
+ .padding(horizontal = 16.dp),
165
+ placeholder = {
166
+ Text(
167
+ style = TextStyle(
168
+ fontSize = 18.sp,
169
+ ),
170
+ text = "Search"
171
+ )
172
+ },
173
+ leadingIcon = {
174
+ Icon(
175
+ imageVector = Icons.Rounded.Search,
176
+ contentDescription = null,
177
+ tint = MaterialTheme.colorScheme.onSurface,
178
+ )
179
+ },
180
+ trailingIcon = {
181
+ IconButton(
182
+ onClick = {
183
+ onClose()
184
+ },
185
+ ) {
186
+ Icon(
187
+ imageVector = Icons.Rounded.Close,
188
+ contentDescription = "Close",
189
+ tint = MaterialTheme.colorScheme.onSurface,
190
+ )
191
+ }
192
+ },
193
+ tonalElevation = 0.dp,
194
+ ) {
195
+ content()
196
+ }
197
+ }
198
+ }
199
+
140
200
  @OptIn(ExperimentalMaterial3Api::class)
141
201
  @Composable
142
202
  fun ChapterScreen(
@@ -148,12 +208,8 @@ fun ChapterScreen(
148
208
  openDrawer: (MenuType, Int) -> Job,
149
209
  ) {
150
210
  val context = LocalContext.current
151
- val isLight = isLightTheme(model.uiMode, isSystemInDarkTheme())
211
+ val verses by model.verses.collectAsState()
152
- val fontType = FontType.valueOf(model.fontType)
212
+ val bookNames by model.bookNames.collectAsState()
153
- val fontSizeDelta = model.fontSizeDelta
154
- val boldWeight = if (model.fontBoldEnabled) FontWeight.W700 else FontWeight.W400
155
- val chapterVerses =
156
- model.verses.filter { it.bookIndex == bookIndex && it.chapterIndex == chapterIndex }
157
213
  val scope = rememberCoroutineScope()
158
214
  var selectedVerses by rememberSaveable {
159
215
  mutableStateOf(listOf<Verse>())
@@ -161,7 +217,14 @@ fun ChapterScreen(
161
217
  var isPlaying by rememberSaveable {
162
218
  mutableStateOf(false)
163
219
  }
164
- var expanded by remember { mutableStateOf(false) }
220
+ val searchText by model.searchText.collectAsState()
221
+ val isSearching by model.isSearching.collectAsState()
222
+ val versesList by model.versesList.collectAsState()
223
+ val fontType = FontType.valueOf(model.fontType)
224
+ val fontSizeDelta = model.fontSizeDelta
225
+ val headingColor = MaterialTheme.colorScheme.onSurface // MaterialTheme.colorScheme.primary,
226
+ val chapterVerses =
227
+ verses.filter { it.bookIndex == bookIndex && it.chapterIndex == chapterIndex }
165
228
  DisposableEffect(Unit) {
166
229
  val started = { _: Any, _: SpeechSynthesisEventArgs ->
167
230
  isPlaying = true
@@ -180,16 +243,48 @@ fun ChapterScreen(
180
243
  LaunchedEffect(key1 = chapterVerses) {
181
244
  selectedVerses = listOf()
182
245
  }
183
- val headingColor = MaterialTheme.colorScheme.onSurface // MaterialTheme.colorScheme.primary,
184
- val view = LocalView.current
185
- val window = (view.context as Activity).window
186
- // WindowInsets.Companion.navigationBars.getBottom()
187
- // WindowInsets.safeContent.getBottom()
188
- // WindowCompat.getInsetsController(window, view).systemBarsBehavior
189
246
  Scaffold(
190
247
  modifier = Modifier
191
248
  .fillMaxSize(),
192
249
  topBar = {
250
+ if (isSearching) {
251
+ EmbeddedSearchBar(
252
+ query = searchText,
253
+ onQueryChange = model::onSearchTextChange,
254
+ onSearch = model::onSearchTextChange,
255
+ onClose = { model.onCloseSearch() }
256
+ ) {
257
+ val groups = versesList.groupBy { "${it.bookName} ${it.chapterIndex + 1}" }
258
+ LazyColumn {
259
+ groups.forEach {
260
+ item(
261
+ contentType = "header"
262
+ ) {
263
+ Text(
264
+ modifier = Modifier.padding(
265
+ vertical = 12.dp,
266
+ ),
267
+ style = TextStyle(
268
+ fontFamily = fontType.family(),
269
+ fontSize = (16 + fontSizeDelta).sp,
270
+ fontWeight = FontWeight.W700,
271
+ color = headingColor,
272
+ ),
273
+ text = it.key,
274
+ )
275
+ }
276
+ items(it.value) { v ->
277
+ VerseView(
278
+ model = model,
279
+ verse = v,
280
+ selectedVerses = selectedVerses,
281
+ setSelectedVerses = { selectedVerses = it },
282
+ )
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
193
288
  TopAppBar(
194
289
  modifier = Modifier
195
290
  .height(72.dp),
@@ -204,7 +299,7 @@ fun ChapterScreen(
204
299
  modifier = Modifier.clickable {
205
300
  openDrawer(MenuType.Book, bookIndex)
206
301
  },
207
- text = model.bookNames[bookIndex],
302
+ text = bookNames[bookIndex],
208
303
  style = TextStyle(
209
304
  fontSize = 22.sp,
210
305
  fontWeight = FontWeight.W500,
@@ -224,6 +319,15 @@ fun ChapterScreen(
224
319
  }
225
320
  },
226
321
  actions = {
322
+ IconButton(
323
+ onClick = { model.onOpenSearch() },
324
+ ) {
325
+ Icon(
326
+ imageVector = Icons.Rounded.Search,
327
+ contentDescription = "Search",
328
+ tint = headingColor,
329
+ )
330
+ }
227
331
  TextButton(onClick = { openDrawer(MenuType.Bible, bookIndex) }) {
228
332
  Text(
229
333
  text = context.getCurrentLocale().language.uppercase(),
@@ -363,87 +467,108 @@ fun ChapterScreen(
363
467
  text = v.heading.replace("<br>", "\n")
364
468
  )
365
469
  }
366
- val isSelected = selectedVerses.contains(v);
367
- val buttonInteractionSource = remember { MutableInteractionSource() }
368
- Text(
470
+ VerseView(
369
- modifier = Modifier
471
+ model = model,
370
- .clickable(
472
+ verse = v,
371
- interactionSource = buttonInteractionSource,
473
+ selectedVerses = selectedVerses,
372
- indication = null
373
- ) {
374
- selectedVerses = if (selectedVerses.contains(v)) {
474
+ setSelectedVerses = { selectedVerses = it },
375
- selectedVerses - v
376
- } else {
475
+ )
377
- selectedVerses + v
378
- }
476
+ }
379
- },
380
- style = TextStyle(
381
- background = if (isSelected)
382
- MaterialTheme.colorScheme.outline
383
- else
384
- Color.Unspecified,
385
- fontFamily = fontType.family(),
386
- color = if (isLight)
387
- Color(0xFF000104)
388
- else
389
- Color(0xFFBCBCBC),
390
- fontWeight = boldWeight,
391
- fontSize = (17 + fontSizeDelta).sp,
392
- lineHeight = (23 + fontSizeDelta).sp,
393
- letterSpacing = 0.sp,
394
- ),
395
- text = buildAnnotatedString {
396
- val spanned = Html.fromHtml(v.text, Html.FROM_HTML_MODE_COMPACT)
397
- val spans = spanned.getSpans(0, spanned.length, Any::class.java)
398
- val verseNo = "${v.verseIndex + 1} "
399
- withStyle(
400
- style = SpanStyle(
401
- fontSize = (13 + fontSizeDelta).sp,
402
- color = if (isLight)
403
- Color(0xFFA20101)
404
- else
405
- Color(0xFFCCCCCC),
406
- fontWeight = FontWeight.W700,
407
- )
408
- ) {
409
- append(verseNo)
410
- }
477
+ }
411
- append(spanned.toString())
412
- spans
413
- .filter { it !is BulletSpan }
414
- .forEach { span ->
415
- val start = spanned.getSpanStart(span)
416
- val end = spanned.getSpanEnd(span)
417
- when (span) {
418
- is ForegroundColorSpan ->
419
- if (isLight) SpanStyle(color = Color(0xFFFF0000))
420
- else SpanStyle(color = Color(0xFFFF636B))
421
-
422
- is StyleSpan -> when (span.style) {
423
- Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
424
- Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
425
- Typeface.BOLD_ITALIC -> SpanStyle(
426
- fontWeight = FontWeight.Bold,
427
- fontStyle = FontStyle.Italic,
428
- )
429
-
430
- else -> null
431
- }
478
+ }
479
+ }
432
480
 
481
+ @Composable
482
+ private fun VerseView(
483
+ model: AppViewModel,
433
- else -> {
484
+ verse: Verse,
434
- null
435
- }
436
- }?.let { spanStyle ->
485
+ selectedVerses: List<Verse>,
486
+ setSelectedVerses: (List<Verse>) -> Unit,
487
+ ) {
488
+ val isLight = isLightTheme(model.uiMode, isSystemInDarkTheme())
489
+ val fontType = FontType.valueOf(model.fontType)
490
+ val fontSizeDelta = model.fontSizeDelta
491
+ val boldWeight = if (model.fontBoldEnabled) FontWeight.W700 else FontWeight.W400
492
+ val buttonInteractionSource = remember { MutableInteractionSource() }
493
+ val isSelected = selectedVerses.contains(verse);
494
+ Text(
495
+ modifier = Modifier
437
- addStyle(
496
+ .clickable(
497
+ interactionSource = buttonInteractionSource,
498
+ indication = null
499
+ ) {
438
- spanStyle,
500
+ setSelectedVerses(
439
- start + verseNo.length - 1,
501
+ if (selectedVerses.contains(verse)) {
440
- end + verseNo.length
502
+ selectedVerses - verse
441
- )
442
- }
503
+ } else {
443
- }
504
+ selectedVerses + verse
444
505
  }
445
506
  )
507
+ },
508
+ style = TextStyle(
509
+ background = if (isSelected)
510
+ MaterialTheme.colorScheme.outline
511
+ else
512
+ Color.Unspecified,
513
+ fontFamily = fontType.family(),
514
+ color = if (isLight)
515
+ Color(0xFF000104)
516
+ else
517
+ Color(0xFFBCBCBC),
518
+ fontWeight = boldWeight,
519
+ fontSize = (17 + fontSizeDelta).sp,
520
+ lineHeight = (23 + fontSizeDelta).sp,
521
+ letterSpacing = 0.sp,
522
+ ),
523
+ text = buildAnnotatedString {
524
+ val spanned = Html.fromHtml(verse.text, Html.FROM_HTML_MODE_COMPACT)
525
+ val spans = spanned.getSpans(0, spanned.length, Any::class.java)
526
+ val verseNo = "${verse.verseIndex + 1} "
527
+ withStyle(
528
+ style = SpanStyle(
529
+ fontSize = (13 + fontSizeDelta).sp,
530
+ color = if (isLight)
531
+ Color(0xFFA20101)
532
+ else
533
+ Color(0xFFCCCCCC),
534
+ fontWeight = FontWeight.W700,
535
+ )
536
+ ) {
537
+ append(verseNo)
446
538
  }
539
+ append(spanned.toString())
540
+ spans
541
+ .filter { it !is BulletSpan }
542
+ .forEach { span ->
543
+ val start = spanned.getSpanStart(span)
544
+ val end = spanned.getSpanEnd(span)
545
+ when (span) {
546
+ is ForegroundColorSpan ->
547
+ if (isLight) SpanStyle(color = Color(0xFFFF0000))
548
+ else SpanStyle(color = Color(0xFFFF636B))
549
+
550
+ is StyleSpan -> when (span.style) {
551
+ Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
552
+ Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
553
+ Typeface.BOLD_ITALIC -> SpanStyle(
554
+ fontWeight = FontWeight.Bold,
555
+ fontStyle = FontStyle.Italic,
556
+ )
557
+
558
+ else -> null
559
+ }
560
+
561
+ else -> {
562
+ null
563
+ }
564
+ }?.let { spanStyle ->
565
+ addStyle(
566
+ spanStyle,
567
+ start + verseNo.length - 1,
568
+ end + verseNo.length
569
+ )
570
+ }
571
+ }
447
572
  }
448
- }
573
+ )
449
574
  }
app/src/main/java/dev/pyrossh/onlyBible/domain/Verse.kt CHANGED
@@ -1,6 +1,8 @@
1
1
  package dev.pyrossh.onlyBible.domain
2
2
 
3
3
  import android.os.Parcelable
4
+ import androidx.appsearch.annotation.Document
5
+ import androidx.appsearch.app.AppSearchSchema
4
6
  import kotlinx.parcelize.Parcelize
5
7
  import kotlinx.serialization.Serializable
6
8