~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.


9d0ddc1e Peter John

1 year ago
fix search focus, add popup toolbar
app/src/main/java/dev/pyrossh/onlyBible/AppViewModel.kt CHANGED
@@ -23,6 +23,7 @@ import androidx.datastore.preferences.preferencesDataStore
23
23
  import androidx.lifecycle.AndroidViewModel
24
24
  import androidx.lifecycle.viewModelScope
25
25
  import com.microsoft.cognitiveservices.speech.SpeechConfig
26
+ import com.microsoft.cognitiveservices.speech.SpeechSynthesisEventArgs
26
27
  import com.microsoft.cognitiveservices.speech.SpeechSynthesizer
27
28
  import dev.pyrossh.onlyBible.domain.Verse
28
29
  import kotlinx.coroutines.CoroutineScope
@@ -53,7 +54,18 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
53
54
  "centralindia"
54
55
  )
55
56
  )
57
+ init {
58
+ val started = { _: Any, _: SpeechSynthesisEventArgs ->
59
+ isPlaying = true
60
+ }
61
+ val completed = { _: Any, _: SpeechSynthesisEventArgs ->
62
+ isPlaying = false
63
+ }
64
+ speechService.SynthesisStarted.addEventListener(started)
65
+ speechService.SynthesisCompleted.addEventListener(completed)
66
+ }
56
67
  var isLoading by mutableStateOf(true)
68
+ var isPlaying by mutableStateOf(false)
57
69
  var isOnError by mutableStateOf(false)
58
70
  val verses = MutableStateFlow(listOf<Verse>())
59
71
  val bookNames = MutableStateFlow(listOf<String>())
app/src/main/java/dev/pyrossh/onlyBible/ChapterScreen.kt CHANGED
@@ -1,19 +1,11 @@
1
1
  package dev.pyrossh.onlyBible
2
2
 
3
- import android.graphics.Typeface
4
3
  import android.os.Parcelable
5
- import android.text.Html
6
- import android.text.style.BulletSpan
7
- import android.text.style.ForegroundColorSpan
8
- import android.text.style.StyleSpan
9
4
  import androidx.compose.animation.AnimatedContentTransitionScope
10
5
  import androidx.compose.animation.AnimatedVisibility
11
6
  import androidx.compose.animation.slideInVertically
12
7
  import androidx.compose.animation.slideOutVertically
13
- import androidx.compose.foundation.clickable
14
8
  import androidx.compose.foundation.gestures.detectDragGestures
15
- import androidx.compose.foundation.interaction.MutableInteractionSource
16
- import androidx.compose.foundation.isSystemInDarkTheme
17
9
  import androidx.compose.foundation.layout.Arrangement
18
10
  import androidx.compose.foundation.layout.PaddingValues
19
11
  import androidx.compose.foundation.layout.Row
@@ -49,9 +41,9 @@ import androidx.compose.material3.Text
49
41
  import androidx.compose.material3.TextButton
50
42
  import androidx.compose.material3.TopAppBar
51
43
  import androidx.compose.runtime.Composable
52
- import androidx.compose.runtime.DisposableEffect
53
44
  import androidx.compose.runtime.LaunchedEffect
54
45
  import androidx.compose.runtime.MutableIntState
46
+ import androidx.compose.runtime.SideEffect
55
47
  import androidx.compose.runtime.collectAsState
56
48
  import androidx.compose.runtime.getValue
57
49
  import androidx.compose.runtime.mutableIntStateOf
@@ -62,20 +54,18 @@ import androidx.compose.runtime.saveable.rememberSaveable
62
54
  import androidx.compose.runtime.setValue
63
55
  import androidx.compose.ui.Alignment
64
56
  import androidx.compose.ui.Modifier
57
+ import androidx.compose.ui.focus.FocusRequester
58
+ import androidx.compose.ui.focus.focusRequester
65
59
  import androidx.compose.ui.graphics.Color
66
60
  import androidx.compose.ui.input.pointer.PointerInputScope
67
61
  import androidx.compose.ui.input.pointer.pointerInput
68
62
  import androidx.compose.ui.platform.LocalContext
69
63
  import androidx.compose.ui.platform.LocalDensity
70
- import androidx.compose.ui.text.SpanStyle
71
64
  import androidx.compose.ui.text.TextStyle
72
- import androidx.compose.ui.text.buildAnnotatedString
73
- import androidx.compose.ui.text.font.FontStyle
74
65
  import androidx.compose.ui.text.font.FontWeight
75
- import androidx.compose.ui.text.withStyle
76
66
  import androidx.compose.ui.unit.dp
77
67
  import androidx.compose.ui.unit.sp
78
- import com.microsoft.cognitiveservices.speech.SpeechSynthesisEventArgs
68
+ import dev.pyrossh.onlyBible.composables.VerseView
79
69
  import dev.pyrossh.onlyBible.domain.Verse
80
70
  import kotlinx.coroutines.Dispatchers
81
71
  import kotlinx.coroutines.Job
@@ -150,6 +140,10 @@ fun EmbeddedSearchBar(
150
140
  onClose: () -> Unit,
151
141
  content: @Composable () -> Unit
152
142
  ) {
143
+ val textFieldFocusRequester = remember { FocusRequester() }
144
+ SideEffect {
145
+ textFieldFocusRequester.requestFocus()
146
+ }
153
147
  ProvideTextStyle(
154
148
  value = TextStyle(
155
149
  fontSize = 18.sp,
@@ -157,14 +151,15 @@ fun EmbeddedSearchBar(
157
151
  )
158
152
  ) {
159
153
  SearchBar(
154
+ modifier = Modifier
155
+ .fillMaxWidth()
156
+ .padding(horizontal = 16.dp)
157
+ .focusRequester(textFieldFocusRequester),
160
158
  query = query,
161
159
  onQueryChange = onQueryChange,
162
160
  onSearch = onSearch,
163
161
  active = true,
164
162
  onActiveChange = {},
165
- modifier = Modifier
166
- .fillMaxWidth()
167
- .padding(horizontal = 16.dp),
168
163
  placeholder = {
169
164
  Text(
170
165
  style = TextStyle(
@@ -228,21 +223,6 @@ fun ChapterScreen(
228
223
  val headingColor = MaterialTheme.colorScheme.onSurface // MaterialTheme.colorScheme.primary,
229
224
  val chapterVerses =
230
225
  verses.filter { it.bookIndex == bookIndex && it.chapterIndex == chapterIndex }
231
- DisposableEffect(Unit) {
232
- val started = { _: Any, _: SpeechSynthesisEventArgs ->
233
- isPlaying = true
234
- }
235
- val completed = { _: Any, _: SpeechSynthesisEventArgs ->
236
- isPlaying = false
237
- }
238
- model.speechService.SynthesisStarted.addEventListener(started)
239
- model.speechService.SynthesisCompleted.addEventListener(completed)
240
-
241
- onDispose {
242
- model.speechService.SynthesisStarted.removeEventListener(started)
243
- model.speechService.SynthesisCompleted.removeEventListener(completed)
244
- }
245
- }
246
226
  LaunchedEffect(key1 = chapterVerses) {
247
227
  selectedVerses = listOf()
248
228
  }
@@ -368,7 +348,7 @@ fun ChapterScreen(
368
348
  modifier = Modifier
369
349
  .height(104.dp)
370
350
  .padding(bottom = bottomPadding),
371
- visible = selectedVerses.isNotEmpty(),
351
+ visible = false,
372
352
  enter = slideInVertically(initialOffsetY = { it / 2 + bottomOffset }),
373
353
  exit = slideOutVertically(targetOffsetY = { it / 2 + bottomOffset }),
374
354
  ) {
@@ -433,14 +413,6 @@ fun ChapterScreen(
433
413
  )
434
414
  }
435
415
  }
436
- // IconButton(onClick = {}) {
437
- // Icon(
438
- // Icons.Filled.Circle,
439
- // contentDescription = "",
440
- // modifier = Modifier.size(64.dp),
441
- // tint = Color.Yellow
442
- // )
443
- // }
444
416
  }
445
417
  }
446
418
  },
@@ -485,99 +457,4 @@ fun ChapterScreen(
485
457
  }
486
458
  }
487
459
  }
488
- }
489
-
490
- @Composable
491
- private fun VerseView(
492
- model: AppViewModel,
493
- verse: Verse,
494
- selectedVerses: List<Verse>,
495
- setSelectedVerses: (List<Verse>) -> Unit,
496
- ) {
497
- val isLight = isLightTheme(model.uiMode, isSystemInDarkTheme())
498
- val fontType = FontType.valueOf(model.fontType)
499
- val fontSizeDelta = model.fontSizeDelta
500
- val boldWeight = if (model.fontBoldEnabled) FontWeight.W700 else FontWeight.W400
501
- val buttonInteractionSource = remember { MutableInteractionSource() }
502
- val isSelected = selectedVerses.contains(verse);
503
- Text(
504
- modifier = Modifier
505
- .clickable(
506
- interactionSource = buttonInteractionSource,
507
- indication = null
508
- ) {
509
- setSelectedVerses(
510
- if (selectedVerses.contains(verse)) {
511
- selectedVerses - verse
512
- } else {
513
- selectedVerses + verse
514
- }
515
- )
516
- },
517
- style = TextStyle(
518
- background = if (isSelected)
519
- MaterialTheme.colorScheme.outline
520
- else
521
- Color.Unspecified,
522
- fontFamily = fontType.family(),
523
- color = if (isLight)
524
- Color(0xFF000104)
525
- else
526
- Color(0xFFBCBCBC),
527
- fontWeight = boldWeight,
528
- fontSize = (17 + fontSizeDelta).sp,
529
- lineHeight = (23 + fontSizeDelta).sp,
530
- letterSpacing = 0.sp,
531
- ),
532
- text = buildAnnotatedString {
533
- val spanned = Html.fromHtml(verse.text, Html.FROM_HTML_MODE_COMPACT)
534
- val spans = spanned.getSpans(0, spanned.length, Any::class.java)
535
- val verseNo = "${verse.verseIndex + 1} "
536
- withStyle(
537
- style = SpanStyle(
538
- fontSize = (13 + fontSizeDelta).sp,
539
- color = if (isLight)
540
- Color(0xFFA20101)
541
- else
542
- Color(0xFFCCCCCC),
543
- fontWeight = FontWeight.W700,
544
- )
545
- ) {
546
- append(verseNo)
547
- }
548
- append(spanned.toString())
549
- spans
550
- .filter { it !is BulletSpan }
551
- .forEach { span ->
552
- val start = spanned.getSpanStart(span)
553
- val end = spanned.getSpanEnd(span)
554
- when (span) {
555
- is ForegroundColorSpan ->
556
- if (isLight) SpanStyle(color = Color(0xFFFF0000))
557
- else SpanStyle(color = Color(0xFFFF636B))
558
-
559
- is StyleSpan -> when (span.style) {
560
- Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
561
- Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
562
- Typeface.BOLD_ITALIC -> SpanStyle(
563
- fontWeight = FontWeight.Bold,
564
- fontStyle = FontStyle.Italic,
565
- )
566
-
567
- else -> null
568
- }
569
-
570
- else -> {
571
- null
572
- }
573
- }?.let { spanStyle ->
574
- addStyle(
575
- spanStyle,
576
- start + verseNo.length - 1,
577
- end + verseNo.length
578
- )
579
- }
580
- }
581
- }
582
- )
583
460
  }
app/src/main/java/dev/pyrossh/onlyBible/composables/VerseView.kt ADDED
@@ -0,0 +1,251 @@
1
+ package dev.pyrossh.onlyBible.composables
2
+
3
+ import android.graphics.Typeface
4
+ import android.text.Html
5
+ import android.text.style.BulletSpan
6
+ import android.text.style.ForegroundColorSpan
7
+ import android.text.style.StyleSpan
8
+ import androidx.compose.foundation.border
9
+ import androidx.compose.foundation.clickable
10
+ import androidx.compose.foundation.interaction.MutableInteractionSource
11
+ import androidx.compose.foundation.isSystemInDarkTheme
12
+ import androidx.compose.foundation.layout.Arrangement
13
+ import androidx.compose.foundation.layout.Row
14
+ import androidx.compose.foundation.layout.fillMaxSize
15
+ import androidx.compose.foundation.layout.height
16
+ import androidx.compose.foundation.layout.width
17
+ import androidx.compose.foundation.shape.RoundedCornerShape
18
+ import androidx.compose.material.icons.Icons
19
+ import androidx.compose.material.icons.filled.Circle
20
+ import androidx.compose.material.icons.outlined.Cancel
21
+ import androidx.compose.material.icons.outlined.PauseCircle
22
+ import androidx.compose.material.icons.outlined.PlayCircle
23
+ import androidx.compose.material.icons.outlined.Share
24
+ import androidx.compose.material3.Icon
25
+ import androidx.compose.material3.IconButton
26
+ import androidx.compose.material3.MaterialTheme
27
+ import androidx.compose.material3.Surface
28
+ import androidx.compose.material3.Text
29
+ import androidx.compose.runtime.Composable
30
+ import androidx.compose.runtime.getValue
31
+ import androidx.compose.runtime.mutableIntStateOf
32
+ import androidx.compose.runtime.remember
33
+ import androidx.compose.runtime.rememberCoroutineScope
34
+ import androidx.compose.runtime.setValue
35
+ import androidx.compose.ui.Alignment
36
+ import androidx.compose.ui.Modifier
37
+ import androidx.compose.ui.draw.shadow
38
+ import androidx.compose.ui.graphics.Color
39
+ import androidx.compose.ui.layout.onPlaced
40
+ import androidx.compose.ui.layout.positionInRoot
41
+ import androidx.compose.ui.platform.LocalContext
42
+ import androidx.compose.ui.text.SpanStyle
43
+ import androidx.compose.ui.text.TextStyle
44
+ import androidx.compose.ui.text.buildAnnotatedString
45
+ import androidx.compose.ui.text.font.FontStyle
46
+ import androidx.compose.ui.text.font.FontWeight
47
+ import androidx.compose.ui.text.withStyle
48
+ import androidx.compose.ui.unit.IntOffset
49
+ import androidx.compose.ui.unit.dp
50
+ import androidx.compose.ui.unit.sp
51
+ import androidx.compose.ui.window.Popup
52
+ import dev.pyrossh.onlyBible.AppViewModel
53
+ import dev.pyrossh.onlyBible.FontType
54
+ import dev.pyrossh.onlyBible.R
55
+ import dev.pyrossh.onlyBible.domain.Verse
56
+ import dev.pyrossh.onlyBible.isLightTheme
57
+ import dev.pyrossh.onlyBible.shareVerses
58
+ import kotlinx.coroutines.Dispatchers
59
+ import kotlinx.coroutines.launch
60
+
61
+ @Composable
62
+ fun VerseView(
63
+ model: AppViewModel,
64
+ verse: Verse,
65
+ selectedVerses: List<Verse>,
66
+ setSelectedVerses: (List<Verse>) -> Unit,
67
+ ) {
68
+ var barYPosition by remember {
69
+ mutableIntStateOf(0)
70
+ }
71
+ val scope = rememberCoroutineScope()
72
+ val context = LocalContext.current
73
+ val isLight = isLightTheme(model.uiMode, isSystemInDarkTheme())
74
+ val fontType = FontType.valueOf(model.fontType)
75
+ val fontSizeDelta = model.fontSizeDelta
76
+ val boldWeight = if (model.fontBoldEnabled) FontWeight.W700 else FontWeight.W400
77
+ val buttonInteractionSource = remember { MutableInteractionSource() }
78
+ val isSelected = selectedVerses.contains(verse)
79
+ Text(
80
+ modifier = Modifier
81
+ .onPlaced {
82
+ barYPosition = it.positionInRoot().y.toInt() + it.size.height
83
+ }
84
+ .clickable(
85
+ interactionSource = buttonInteractionSource,
86
+ indication = null
87
+ ) {
88
+ setSelectedVerses(
89
+ if (selectedVerses.contains(verse)) {
90
+ selectedVerses - verse
91
+ } else {
92
+ selectedVerses + verse
93
+ }
94
+ )
95
+ },
96
+ style = TextStyle(
97
+ background = if (isSelected)
98
+ MaterialTheme.colorScheme.outline
99
+ else
100
+ Color.Unspecified,
101
+ fontFamily = fontType.family(),
102
+ color = if (isLight)
103
+ Color(0xFF000104)
104
+ else
105
+ Color(0xFFBCBCBC),
106
+ fontWeight = boldWeight,
107
+ fontSize = (17 + fontSizeDelta).sp,
108
+ lineHeight = (23 + fontSizeDelta).sp,
109
+ letterSpacing = 0.sp,
110
+ ),
111
+ text = buildAnnotatedString {
112
+ val spanned = Html.fromHtml(verse.text, Html.FROM_HTML_MODE_COMPACT)
113
+ val spans = spanned.getSpans(0, spanned.length, Any::class.java)
114
+ val verseNo = "${verse.verseIndex + 1} "
115
+ withStyle(
116
+ style = SpanStyle(
117
+ fontSize = (13 + fontSizeDelta).sp,
118
+ color = if (isLight)
119
+ Color(0xFFA20101)
120
+ else
121
+ Color(0xFFCCCCCC),
122
+ fontWeight = FontWeight.W700,
123
+ )
124
+ ) {
125
+ append(verseNo)
126
+ }
127
+ append(spanned.toString())
128
+ spans
129
+ .filter { it !is BulletSpan }
130
+ .forEach { span ->
131
+ val start = spanned.getSpanStart(span)
132
+ val end = spanned.getSpanEnd(span)
133
+ when (span) {
134
+ is ForegroundColorSpan ->
135
+ if (isLight) SpanStyle(color = Color(0xFFFF0000))
136
+ else SpanStyle(color = Color(0xFFFF636B))
137
+
138
+ is StyleSpan -> when (span.style) {
139
+ Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
140
+ Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
141
+ Typeface.BOLD_ITALIC -> SpanStyle(
142
+ fontWeight = FontWeight.Bold,
143
+ fontStyle = FontStyle.Italic,
144
+ )
145
+
146
+ else -> null
147
+ }
148
+
149
+ else -> {
150
+ null
151
+ }
152
+ }?.let { spanStyle ->
153
+ addStyle(
154
+ spanStyle,
155
+ start + verseNo.length - 1,
156
+ end + verseNo.length
157
+ )
158
+ }
159
+ }
160
+ }
161
+ )
162
+ if (isSelected && selectedVerses.last() == verse) {
163
+ Popup(
164
+ alignment = Alignment.TopCenter,
165
+ offset = IntOffset(0, y = barYPosition),
166
+ ) {
167
+ Surface(
168
+ modifier = Modifier
169
+ .width(300.dp)
170
+ .height(60.dp)
171
+ .border(
172
+ width = 1.dp,
173
+ color = MaterialTheme.colorScheme.outline,
174
+ shape = RoundedCornerShape(2.dp)
175
+ )
176
+ .shadow(elevation = 2.dp, shape = RoundedCornerShape(2.dp)),
177
+ ) {
178
+ Row(
179
+ modifier = Modifier
180
+ .fillMaxSize(),
181
+ horizontalArrangement = Arrangement.SpaceAround,
182
+ verticalAlignment = Alignment.CenterVertically,
183
+ ) {
184
+ IconButton(onClick = {
185
+ setSelectedVerses(listOf())
186
+ }) {
187
+ Icon(
188
+ // modifier = Modifier.size(36.dp),
189
+ imageVector = Icons.Outlined.Cancel,
190
+ contentDescription = "Clear",
191
+ )
192
+ }
193
+ IconButton(onClick = {}) {
194
+ Icon(
195
+ Icons.Filled.Circle,
196
+ contentDescription = "",
197
+ tint = Color.Yellow
198
+ )
199
+ }
200
+ IconButton(onClick = {}) {
201
+ Icon(
202
+ Icons.Filled.Circle,
203
+ contentDescription = "",
204
+ tint = Color.Cyan
205
+ )
206
+ }
207
+ IconButton(onClick = {}) {
208
+ Icon(
209
+ Icons.Filled.Circle,
210
+ contentDescription = "",
211
+ tint = Color.Magenta
212
+ )
213
+ }
214
+ IconButton(onClick = {
215
+ if (model.isPlaying) {
216
+ model.speechService.StopSpeakingAsync()
217
+ } else {
218
+ scope.launch(Dispatchers.IO) {
219
+ for (v in selectedVerses.sortedBy { it.verseIndex }) {
220
+ model.speechService.StartSpeakingSsml(
221
+ v.toSSML(context.getString(R.string.voice)),
222
+ )
223
+ }
224
+ }
225
+ }
226
+ }) {
227
+ Icon(
228
+ // modifier = Modifier.size(36.dp),
229
+ imageVector = if (model.isPlaying)
230
+ Icons.Outlined.PauseCircle
231
+ else
232
+ Icons.Outlined.PlayCircle,
233
+ contentDescription = "Audio",
234
+ )
235
+ }
236
+ IconButton(onClick = {
237
+ shareVerses(
238
+ context,
239
+ selectedVerses.sortedBy { it.verseIndex })
240
+ }) {
241
+ Icon(
242
+ // modifier = Modifier.size(32.dp),
243
+ imageVector = Icons.Outlined.Share,
244
+ contentDescription = "Share",
245
+ )
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }