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


4a2e3b1b pyrossh

1 year ago
implement Highlighting
composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/composables/VerseHeading.kt CHANGED
@@ -1,15 +1,24 @@
1
1
  package dev.pyrossh.only_bible_app.composables
2
2
 
3
3
  import androidx.compose.foundation.layout.padding
4
+ import androidx.compose.foundation.text.ClickableText
4
5
  import androidx.compose.material3.MaterialTheme
5
- import androidx.compose.material3.Text
6
6
  import androidx.compose.runtime.Composable
7
7
  import androidx.compose.ui.Modifier
8
+ import androidx.compose.ui.graphics.Color
9
+ import androidx.compose.ui.text.SpanStyle
8
10
  import androidx.compose.ui.text.TextStyle
11
+ import androidx.compose.ui.text.buildAnnotatedString
12
+ import androidx.compose.ui.text.font.FontStyle
9
13
  import androidx.compose.ui.text.font.FontWeight
14
+ import androidx.compose.ui.text.withStyle
10
15
  import androidx.compose.ui.unit.dp
11
16
  import androidx.compose.ui.unit.sp
12
17
  import dev.pyrossh.only_bible_app.FontType
18
+ import dev.pyrossh.only_bible_app.screens.ChapterScreenProps
19
+ import dev.pyrossh.only_bible_app.utils.SimpleParser
20
+ import dev.pyrossh.only_bible_app.utils.TagNode
21
+ import dev.pyrossh.only_bible_app.utils.TextNode
13
22
  import utils.LocalNavController
14
23
 
15
24
  @Composable
@@ -19,7 +28,49 @@ fun VerseHeading(
19
28
  fontSizeDelta: Int,
20
29
  ) {
21
30
  val navController = LocalNavController.current
31
+ val nodes = SimpleParser(text).parse()
32
+ val annotatedString = buildAnnotatedString {
33
+ for (n in nodes) {
34
+ if (n is TextNode) {
35
+ append(n.value)
36
+ } else if (n is TagNode && n.name == "br") {
37
+ append("\n")
38
+ } else if (n is TagNode && n.name == "red") {
39
+ withStyle(
40
+ style = SpanStyle(
41
+ color = Color.Red,
42
+ )
43
+ ) {
44
+ append(n.child!!.value)
45
+ }
46
+ } else if (n is TagNode && n.name == "yellow") {
47
+ withStyle(
48
+ style = SpanStyle(
49
+ background = Color.Yellow,
50
+ )
51
+ ) {
52
+ append(n.child!!.value)
53
+ }
54
+ } else if (n is TagNode && n.name == "a") {
55
+ withStyle(
56
+ style = SpanStyle(
57
+ fontSize = (14 + fontSizeDelta).sp,
58
+ fontStyle = FontStyle.Italic,
59
+ color = Color(0xFF008AE6),
60
+ )
61
+ ) {
62
+ append(n.child!!.value)
63
+ addStringAnnotation(
64
+ tag = "URL",
65
+ annotation = n.attributes["href"]!!,
66
+ start = n.child!!.pos.start,
67
+ end = n.child!!.pos.end,
68
+ )
69
+ }
70
+ }
71
+ }
72
+ }
22
- Text(
73
+ ClickableText(
23
74
  modifier = Modifier.padding(bottom = 12.dp),
24
75
  style = TextStyle(
25
76
  fontFamily = fontType.family(),
@@ -27,28 +78,22 @@ fun VerseHeading(
27
78
  fontWeight = FontWeight.W700,
28
79
  color = MaterialTheme.colorScheme.onSurface,
29
80
  ),
30
- text = text,
81
+ text = annotatedString,
31
- // text = AnnotatedString.fromHtml(
32
- // htmlString = text,
33
- // linkStyles = TextLinkStyles(
34
- // style = SpanStyle(
35
- // fontSize = (14 + fontSizeDelta).sp,
36
- // fontStyle = FontStyle.Italic,
37
- // color = Color(0xFF008AE6),
38
- // )
39
- // ),
82
+ onClick = {
40
- // linkInteractionListener = {
41
- //// view.playSoundEffect(SoundEffectConstants.CLICK)
83
+ // view.playSoundEffect(SoundEffectConstants.CLICK)
84
+ annotatedString
42
- // val url = (it as LinkAnnotation.Url).url
85
+ .getStringAnnotations("URL", it, it)
86
+ .map { anno ->
43
- // val parts = url.split(":")
87
+ val parts = anno.item.split(":")
44
- // navController.navigate(
88
+ navController.navigate(
45
- // ChapterScreenProps(
89
+ ChapterScreenProps(
46
- // bookIndex = parts[0].toInt(),
90
+ bookIndex = parts[0].toInt(),
47
- // chapterIndex = parts[1].toInt(),
91
+ chapterIndex = parts[1].toInt(),
48
- // verseIndex = parts[2].toInt(),
92
+ verseIndex = parts[2].toInt(),
49
- // )
50
- // )
51
- // },
52
- // ),
93
+ )
94
+ )
95
+ }
96
+
97
+ }
53
98
  )
54
99
  }
composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/composables/VerseText.kt CHANGED
@@ -43,6 +43,7 @@ import androidx.compose.ui.layout.positionInRoot
43
43
  import androidx.compose.ui.text.SpanStyle
44
44
  import androidx.compose.ui.text.TextStyle
45
45
  import androidx.compose.ui.text.buildAnnotatedString
46
+ import androidx.compose.ui.text.font.FontStyle
46
47
  import androidx.compose.ui.text.font.FontWeight
47
48
  import androidx.compose.ui.text.withStyle
48
49
  import androidx.compose.ui.unit.IntOffset
@@ -58,6 +59,9 @@ import kotlinx.coroutines.IO
58
59
  import kotlinx.coroutines.launch
59
60
  import dev.pyrossh.only_bible_app.lightHighlights
60
61
  import dev.pyrossh.only_bible_app.rememberShareVerses
62
+ import dev.pyrossh.only_bible_app.utils.SimpleParser
63
+ import dev.pyrossh.only_bible_app.utils.TagNode
64
+ import dev.pyrossh.only_bible_app.utils.TextNode
61
65
  import utils.LocalNavController
62
66
 
63
67
  @Composable
@@ -78,15 +82,15 @@ fun VerseText(
78
82
  val isSelected = selectedVerses.contains(verse)
79
83
  val highlightedColorIndex = model.getHighlightForVerse(verse)
80
84
  val currentHighlightColors = if (isLight) lightHighlights else darkHighlights
81
- val currentHighlightWordKey = if (isLight) "background" else "color"
82
85
  val text = if (highlightWord != null)
83
86
  verse.text.replace(
84
87
  highlightWord,
85
- "<span style=\"${currentHighlightWordKey}: yellow;\">${highlightWord}</span>",
88
+ "<yellow>${highlightWord}</yellow>",
86
89
  true
87
90
  )
88
91
  else
89
92
  verse.text
93
+ val nodes = SimpleParser(text).parse()
90
94
  Text(
91
95
  modifier = Modifier
92
96
  .onPlaced {
@@ -141,22 +145,29 @@ fun VerseText(
141
145
  ) {
142
146
  append("${verse.verseIndex + 1} ")
143
147
  }
148
+ for (n in nodes) {
149
+ if (n is TextNode) {
144
- append(text)
150
+ append(n.value)
145
- // append(
146
- // AnnotatedString.Companion.fromHtml(
147
- // htmlString = text,
148
- // linkStyles = TextLinkStyles(
149
- // style = SpanStyle(
150
- // fontSize = (14 + model.fontSizeDelta).sp,
151
- // fontStyle = FontStyle.Italic,
152
- // color = Color(0xFF008AE6),
153
- // )
154
- // ),
155
- // linkInteractionListener = {
151
+ } else if (n is TagNode && n.name == "br") {
152
+ append("\n")
153
+ } else if (n is TagNode && n.name == "red") {
154
+ withStyle(
155
+ style = SpanStyle(
156
+ color = Color.Red,
157
+ )
158
+ ) {
159
+ append(n.child!!.value)
160
+ }
156
- // println("SOUTT ${(it as LinkAnnotation.Url).url}")
161
+ } else if (n is TagNode && n.name == "yellow") {
157
- // },
162
+ withStyle(
158
- // )
163
+ style = SpanStyle(
159
- // )
164
+ background = Color.Yellow,
165
+ )
166
+ ) {
167
+ append(n.child!!.value)
168
+ }
169
+ }
170
+ }
160
171
  }
161
172
  )
162
173
  if (isSelected && selectedVerses.last() == verse) {
composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/utils/SimpleParser.kt CHANGED
@@ -66,16 +66,18 @@ class SimpleParser(val input: CharSequence) {
66
66
  skip(4)
67
67
  return TagNode(Pos(start, cursor), "br")
68
68
  }
69
- for (t in listOf("red", "yellow", "em")) {
69
+ for (t in listOf("red", "yellow")) {
70
70
  if (input.substring(start, start + 2 + t.length) == "<$t>") {
71
- val redNode = TagNode(Pos(start, 0), t)
72
71
  skip(t.length + 2)
73
72
  val textNode = parseTextNode()
74
73
  if (input.substring(textNode.pos.end, textNode.pos.end + t.length + 3) == "</$t>") {
75
74
  skip(t.length + 3)
76
- redNode.pos = Pos(start, textNode.pos.end + t.length + 3)
77
- redNode.child = textNode
78
- return redNode
75
+ return TagNode(
76
+ Pos(start, textNode.pos.end + t.length + 3),
77
+ t,
78
+ emptyMap(),
79
+ textNode
80
+ )
79
81
  } else {
80
82
  throw RuntimeException(
81
83
  "failed find closing tag for <red> at ${textNode.pos.end} ${
@@ -89,6 +91,57 @@ class SimpleParser(val input: CharSequence) {
89
91
  }
90
92
  }
91
93
 
94
+ if (input.substring(start, start + 2) == "<a") {
95
+ skip(2)
96
+ val attrs = parseAttributes()
97
+ if (consume() != '>') {
98
+ throw RuntimeException("failed to parseTag 'a' close '>'")
99
+ }
100
+ val textNode = parseTextNode()
101
+ if (input.substring(textNode.pos.end, textNode.pos.end + 4) == "</a>") {
102
+ skip(4)
103
+ return TagNode(Pos(start, textNode.pos.end + 4), "a", attrs, textNode)
104
+ } else {
105
+ throw RuntimeException(
106
+ "failed find closing tag for <a> ${textNode.pos.end}"
107
+ )
108
+ }
109
+ }
110
+
92
111
  throw RuntimeException("failed to parseTag at $start ${input.substring(start, start + 10)}")
93
112
  }
113
+
114
+ private fun parseAttributes(): Map<String, String> {
115
+ val start = cursor
116
+ val key = StringBuilder()
117
+ while (hasNext() && peek() != '=') {
118
+ while (peek() == ' ') {
119
+ skip()
120
+ }
121
+ key.append(consume())
122
+ }
123
+ skip()
124
+ while (peek() == ' ') {
125
+ skip()
126
+ }
127
+ val value = parseString()
128
+ if (peek() == '>') {
129
+ return mapOf(Pair(key.toString(), value))
130
+ }
131
+ throw RuntimeException("failed to parseAttribute at $start")
132
+ }
133
+
134
+ private fun parseString(): String {
135
+ val start = cursor
136
+ val result = StringBuilder()
137
+ if (consume() != '"') {
138
+ throw RuntimeException("failed to parseAttribute at ${start + 1}")
139
+ }
140
+
141
+ while (hasNext() && peek() != '"') {
142
+ result.append(consume())
143
+ }
144
+ skip()
145
+ return result.toString()
146
+ }
94
147
  }
composeApp/src/commonTest/kotlin/dev/pyrossh/only_bible_app/utils/SimpleParserTest.kt CHANGED
@@ -9,7 +9,7 @@ class SimpleParserTest {
9
9
  fun parseTextOnly() {
10
10
  val parser = SimpleParser("lorem ipsum 123 dorem")
11
11
  assertEquals(
12
- listOf(TextNode(pos = Pos(0, 12), value = "lorem ipsum 123 dorem")),
12
+ listOf(TextNode(pos = Pos(0, 21), value = "lorem ipsum 123 dorem")),
13
13
  parser.parse(),
14
14
  )
15
15
  }
@@ -30,4 +30,35 @@ class SimpleParserTest {
30
30
  parser.parse(),
31
31
  )
32
32
  }
33
+
34
+ @Test
35
+ fun parseTextA() {
36
+ val parser =
37
+ SimpleParser("The History of Creation <br> (<a href=\"42:0:0\">John 1:1–5</a> ; <a href=\"57:10:1\">Hebrews 11:1–3</a>)|In the beginning God created the heaven and the earth.")
38
+ assertEquals(
39
+ listOf(
40
+ TextNode(pos = Pos(start = 0, end = 24), value = "The History of Creation "),
41
+ TagNode(pos = Pos(start = 24, end = 28), name = "br"),
42
+ TextNode(pos = Pos(start = 28, end = 30), value = " ("),
43
+ TagNode(
44
+ pos = Pos(start = 30, end = 61),
45
+ name = "a",
46
+ attributes = mapOf("href" to "42:0:0"),
47
+ child = TextNode(pos = Pos(start = 47, end = 57), value = "John 1:1–5")
48
+ ),
49
+ TextNode(pos = Pos(start = 61, end = 64), value = " ; "),
50
+ TagNode(
51
+ pos = Pos(start = 64, end = 100),
52
+ name = "a",
53
+ attributes = mapOf("href" to "57:10:1"),
54
+ child = TextNode(pos = Pos(start = 82, end = 96), value = "Hebrews 11:1–3")
55
+ ),
56
+ TextNode(
57
+ pos = Pos(start = 100, end = 156),
58
+ value = ")|In the beginning God created the heaven and the earth."
59
+ ),
60
+ ),
61
+ parser.parse(),
62
+ )
63
+ }
33
64
  }
composeApp/src/iosMain/kotlin/dev/pyrossh/only_bible_app/Platform.ios.kt CHANGED
@@ -1,13 +1,19 @@
1
1
  package dev.pyrossh.only_bible_app
2
2
 
3
+ import androidx.compose.foundation.isSystemInDarkTheme
3
4
  import androidx.compose.runtime.Composable
5
+ import androidx.compose.runtime.LaunchedEffect
4
6
  import androidx.compose.ui.ExperimentalComposeUiApi
7
+ import androidx.compose.ui.graphics.toArgb
5
8
  import androidx.compose.ui.platform.LocalWindowInfo
6
9
  import androidx.compose.ui.unit.Dp
7
10
  import androidx.compose.ui.unit.dp
8
11
  import dev.pyrossh.only_bible_app.config.BuildKonfig
9
12
  import dev.pyrossh.only_bible_app.domain.Verse
10
13
  import platform.UIKit.UIScreen
14
+ import theme.darkScheme
15
+ import theme.lightScheme
16
+
11
17
  //import platform.AVKit.Audio
12
18
 
13
19
  @OptIn(ExperimentalComposeUiApi::class)
@@ -26,7 +32,14 @@ actual fun playClickSound() {
26
32
  // AudioServicesPlayAlertSound(SystemSoundID(1322))
27
33
  }
28
34
 
35
+ @Composable
29
- actual fun shareVerses(verses: List<Verse>) {
36
+ actual fun rememberShareVerses(): (verses: List<Verse>) -> Unit {
37
+ return { verses ->
38
+ }
39
+ }
40
+
41
+ @Composable
42
+ actual fun onThemeChange(themeType: ThemeType) {
30
43
  }
31
44
 
32
45
  actual object SpeechService {