Skip to content

Commit c9cf023

Browse files
leandroBorgesFerreiraLeandro Ferreira
andauthored
Link in text (#510)
* Adding link to text * style for links * code clean * shadow * Link clickable * fixing link * persistence of links --------- Co-authored-by: Leandro Ferreira <[email protected]>
1 parent 049b484 commit c9cf023

10 files changed

Lines changed: 305 additions & 77 deletions

File tree

application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import io.writeopia.common.utils.icons.all.Undo2
5858
import io.writeopia.common.utils.icons.all.WandSparkles
5959
import io.writeopia.common.utils.icons.all.X
6060
import io.writeopia.common.utils.icons.all.Zap
61+
import io.writeopia.common.utils.icons.all.Link
6162

6263
object WrIcons {
6364
val settings: ImageVector = Bolt
@@ -172,6 +173,8 @@ object WrIcons {
172173

173174
val chart = ChartScatter
174175

176+
val link = Link
177+
175178
val allIcons: Map<String, ImageVector> =
176179
mapOf(
177180
"settings" to settings,
@@ -228,6 +231,7 @@ object WrIcons {
228231
"ai" to ai,
229232
"command" to command,
230233
"chart" to chart,
234+
"link" to link,
231235
)
232236

233237
fun fromName(name: String): ImageVector? = allIcons[name]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.writeopia.common.utils.icons.all
2+
3+
import androidx.compose.ui.graphics.Color
4+
import androidx.compose.ui.graphics.SolidColor
5+
import androidx.compose.ui.graphics.StrokeCap
6+
import androidx.compose.ui.graphics.StrokeJoin
7+
import androidx.compose.ui.graphics.vector.ImageVector
8+
import androidx.compose.ui.graphics.vector.path
9+
import androidx.compose.ui.unit.dp
10+
11+
internal val Link: ImageVector
12+
get() {
13+
if (_link != null) return _link!!
14+
15+
_link = ImageVector.Builder(
16+
name = "link",
17+
defaultWidth = 24.dp,
18+
defaultHeight = 24.dp,
19+
viewportWidth = 24f,
20+
viewportHeight = 24f
21+
).apply {
22+
path(
23+
stroke = SolidColor(Color.Black),
24+
strokeLineWidth = 2f,
25+
strokeLineCap = StrokeCap.Round,
26+
strokeLineJoin = StrokeJoin.Round
27+
) {
28+
moveTo(10f, 13f)
29+
arcToRelative(5f, 5f, 0f, false, false, 7.54f, 0.54f)
30+
lineToRelative(3f, -3f)
31+
arcToRelative(5f, 5f, 0f, false, false, -7.07f, -7.07f)
32+
lineToRelative(-1.72f, 1.71f)
33+
}
34+
path(
35+
stroke = SolidColor(Color.Black),
36+
strokeLineWidth = 2f,
37+
strokeLineCap = StrokeCap.Round,
38+
strokeLineJoin = StrokeJoin.Round
39+
) {
40+
moveTo(14f, 11f)
41+
arcToRelative(5f, 5f, 0f, false, false, -7.54f, -0.54f)
42+
lineToRelative(-3f, 3f)
43+
arcToRelative(5f, 5f, 0f, false, false, 7.07f, 7.07f)
44+
lineToRelative(1.71f, -1.71f)
45+
}
46+
}.build()
47+
48+
return _link!!
49+
}
50+
51+
private var _link: ImageVector? = null

writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/span/SpanInfo.kt

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import kotlin.math.abs
44
import kotlin.math.max
55
import kotlin.math.min
66

7-
data class SpanInfo private constructor(val start: Int, val end: Int, val span: Span) {
7+
data class SpanInfo private constructor(
8+
val start: Int,
9+
val end: Int,
10+
val span: Span,
11+
val extra: String? = null
12+
) {
813

914
operator fun plus(spanInfo: SpanInfo) =
1015
if (spanInfo.span == spanInfo.span) {
@@ -29,10 +34,16 @@ data class SpanInfo private constructor(val start: Int, val end: Int, val span:
2934
/**
3035
* Serialize the object as a string: "start:end:span"
3136
*/
32-
fun toText(): String = "$start:$end:${span.toText()}"
37+
fun toText(): String = "$start:$end:${span.toText()}:$extra"
3338

3439
fun size() = abs(end - start)
3540

41+
fun expandable() =
42+
when (this.span) {
43+
Span.LINK -> false
44+
else -> true
45+
}
46+
3647
fun intersection(spanInfo: SpanInfo): Intersection {
3748
val (smaller, bigger) = orderSpansBySize(this, spanInfo)
3849

@@ -80,19 +91,25 @@ data class SpanInfo private constructor(val start: Int, val end: Int, val span:
8091

8192
fun fromString(serialized: String): SpanInfo {
8293
val parts = serialized.split(":")
83-
require(parts.size == 3) { "Invalid serialized format" }
94+
require(parts.size == 3 || parts.size == 4) { "Invalid serialized format" }
8495

8596
val start = parts[0].toIntOrNull() ?: error("Invalid start value")
8697
val end = parts[1].toIntOrNull() ?: error("Invalid end value")
8798
val span = Span.textFromString(parts[2])
99+
val extra = if (parts.size >= 4) parts[3] else null
88100

89-
return SpanInfo(start, end, span)
101+
return SpanInfo(start, end, span, extra)
90102
}
91103

92-
fun create(start: Int, end: Int, span: Span): SpanInfo {
104+
fun create(
105+
start: Int,
106+
end: Int,
107+
span: Span,
108+
extra: String? = null
109+
): SpanInfo {
93110
val (realStart, realEnd) = if (start <= end) start to end else end to start
94111

95-
return SpanInfo(realStart, realEnd, span)
112+
return SpanInfo(realStart, realEnd, span, extra)
96113
}
97114
}
98115
}
@@ -112,6 +129,7 @@ enum class Span(val label: String) {
112129
HIGHLIGHT_YELLOW("HIGHLIGHT"),
113130
HIGHLIGHT_GREEN("HIGHLIGHT_GREEN"),
114131
HIGHLIGHT_RED("HIGHLIGHT_RED"),
132+
LINK("LINK"),
115133
NONE("");
116134

117135
fun toText() = this.label

writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt

Lines changed: 128 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.writeopia.ui.components
22

3+
import androidx.compose.animation.Crossfade
34
import androidx.compose.foundation.clickable
45
import androidx.compose.foundation.horizontalScroll
56
import androidx.compose.foundation.layout.PaddingValues
@@ -9,20 +10,31 @@ import androidx.compose.foundation.layout.padding
910
import androidx.compose.foundation.layout.size
1011
import androidx.compose.foundation.layout.width
1112
import androidx.compose.foundation.rememberScrollState
13+
import androidx.compose.foundation.text.BasicTextField
1214
import androidx.compose.material.icons.Icons
1315
import androidx.compose.material.icons.filled.ContentCut
1416
import androidx.compose.material.icons.filled.DeleteOutline
1517
import androidx.compose.material.icons.outlined.FormatBold
1618
import androidx.compose.material.icons.outlined.FormatItalic
1719
import androidx.compose.material.icons.outlined.FormatUnderlined
20+
import androidx.compose.material3.Card
21+
import androidx.compose.material3.CardDefaults
1822
import androidx.compose.material3.Icon
1923
import androidx.compose.material3.MaterialTheme
2024
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableStateOf
27+
import androidx.compose.runtime.remember
28+
import androidx.compose.runtime.setValue
2129
import androidx.compose.ui.Alignment
2230
import androidx.compose.ui.Modifier
2331
import androidx.compose.ui.draw.clip
32+
import androidx.compose.ui.draw.shadow
33+
import androidx.compose.ui.graphics.SolidColor
2434
import androidx.compose.ui.unit.Dp
2535
import androidx.compose.ui.unit.dp
36+
import androidx.compose.ui.window.Popup
37+
import androidx.compose.ui.window.PopupProperties
2638
import io.writeopia.sdk.models.span.Span
2739
import io.writeopia.ui.icons.WrSdkIcons
2840
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -200,62 +212,87 @@ fun EditionScreenForText(
200212
modifier: Modifier = Modifier,
201213
iconSize: Dp = 36.dp,
202214
onSpanClick: (Span) -> Unit = {},
215+
onLinkConfirm: (String) -> Unit = {},
203216
) {
204217
val iconPadding = PaddingValues(vertical = 4.dp)
205218
val clipShape = MaterialTheme.shapes.medium
206219
val spaceWidth = 8.dp
207220

208-
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
209-
val tint = MaterialTheme.colorScheme.onPrimary
221+
var showLinkScreen by remember { mutableStateOf(false) }
210222

211-
Row(
212-
modifier = Modifier
213-
.horizontalScroll(rememberScrollState()),
214-
verticalAlignment = Alignment.CenterVertically
215-
) {
216-
Icon(
217-
modifier = Modifier
218-
.clip(clipShape)
219-
.clickable {
220-
onSpanClick(Span.BOLD)
221-
}
222-
.size(iconSize)
223-
.padding(iconPadding),
224-
imageVector = Icons.Outlined.FormatBold,
225-
contentDescription = "BOLD",
226-
// contentDescription = stringResource(R.string.delete),
227-
tint = tint
223+
Crossfade(showLinkScreen) { showLink ->
224+
if (showLink) {
225+
AddLinkScreen(
226+
modifier = Modifier,
227+
cancel = { showLinkScreen = false },
228+
onLinkConfirm = onLinkConfirm
228229
)
230+
} else {
231+
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
232+
val tint = MaterialTheme.colorScheme.onPrimary
229233

230-
Icon(
231-
modifier = Modifier
232-
.clip(clipShape)
233-
.clickable {
234-
onSpanClick(Span.ITALIC)
235-
}
236-
.size(iconSize)
237-
.padding(iconPadding),
238-
imageVector = Icons.Outlined.FormatItalic,
239-
contentDescription = "ITALIC",
234+
Row(
235+
modifier = Modifier.horizontalScroll(rememberScrollState()),
236+
verticalAlignment = Alignment.CenterVertically
237+
) {
238+
Icon(
239+
modifier = Modifier
240+
.clip(clipShape)
241+
.clickable {
242+
onSpanClick(Span.BOLD)
243+
}
244+
.size(iconSize)
245+
.padding(iconPadding),
246+
imageVector = Icons.Outlined.FormatBold,
247+
contentDescription = "BOLD",
240248
// contentDescription = stringResource(R.string.delete),
241-
tint = tint
242-
)
249+
tint = tint,
250+
)
243251

244-
Spacer(modifier = Modifier.width(spaceWidth))
252+
Icon(
253+
modifier = Modifier
254+
.clip(clipShape)
255+
.clickable {
256+
onSpanClick(Span.ITALIC)
257+
}
258+
.size(iconSize)
259+
.padding(iconPadding),
260+
imageVector = Icons.Outlined.FormatItalic,
261+
contentDescription = "ITALIC",
262+
// contentDescription = stringResource(R.string.delete),
263+
tint = tint
264+
)
245265

246-
Icon(
247-
modifier = Modifier
248-
.clip(clipShape)
249-
.clickable {
250-
onSpanClick(Span.UNDERLINE)
251-
}
252-
.size(iconSize)
253-
.padding(iconPadding),
254-
imageVector = Icons.Outlined.FormatUnderlined,
255-
contentDescription = "UNDERLINE",
266+
Spacer(modifier = Modifier.width(spaceWidth))
267+
268+
Icon(
269+
modifier = Modifier
270+
.clip(clipShape)
271+
.clickable {
272+
onSpanClick(Span.UNDERLINE)
273+
}
274+
.size(iconSize)
275+
.padding(iconPadding),
276+
imageVector = Icons.Outlined.FormatUnderlined,
277+
contentDescription = "UNDERLINE",
256278
// contentDescription = stringResource(R.string.delete),
257-
tint = tint
258-
)
279+
tint = tint
280+
)
281+
282+
Spacer(modifier = Modifier.width(spaceWidth))
283+
284+
Icon(
285+
modifier = Modifier
286+
.clip(clipShape)
287+
.clickable {
288+
showLinkScreen = true
289+
}
290+
.size(iconSize)
291+
.padding(PaddingValues(vertical = 5.dp)),
292+
imageVector = WrSdkIcons.linkPage,
293+
contentDescription = "Link to page",
294+
tint = tint
295+
)
259296

260297
// Spacer(modifier = Modifier.width(spaceWidth))
261298
//
@@ -295,6 +332,53 @@ fun EditionScreenForText(
295332
// contentDescription = "Link to page",
296333
// tint = tint
297334
// )
335+
}
336+
}
337+
}
338+
}
339+
}
340+
341+
@Composable
342+
private fun AddLinkScreen(
343+
modifier: Modifier = Modifier,
344+
cancel: () -> Unit,
345+
onLinkConfirm: (String) -> Unit = {},
346+
) {
347+
var linkText by remember { mutableStateOf("") }
348+
349+
Popup(properties = PopupProperties(focusable = true), onDismissRequest = cancel) {
350+
Card(modifier = modifier.shadow(elevation = 6.dp, shape = CardDefaults.shape)) {
351+
Row(
352+
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
353+
verticalAlignment = Alignment.CenterVertically
354+
) {
355+
BasicTextField(
356+
modifier = Modifier.width(300.dp),
357+
value = linkText,
358+
onValueChange = { linkText = it },
359+
maxLines = 1,
360+
singleLine = true,
361+
textStyle = MaterialTheme.typography
362+
.bodyMedium
363+
.copy(color = MaterialTheme.colorScheme.onBackground),
364+
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground)
365+
)
366+
367+
Spacer(modifier = Modifier.width(8.dp))
368+
369+
Icon(
370+
imageVector = WrSdkIcons.check,
371+
contentDescription = "Confirm",
372+
modifier = Modifier
373+
.clip(MaterialTheme.shapes.medium)
374+
.clickable {
375+
onLinkConfirm(linkText)
376+
}
377+
.size(28.dp)
378+
.padding(4.dp),
379+
tint = MaterialTheme.colorScheme.onBackground
380+
)
381+
}
298382
}
299383
}
300384
}

writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import io.writeopia.ui.components.EditionScreenForText
2121
fun TextToolbox(
2222
hasSelection: Boolean,
2323
onSpanClick: (Span) -> Unit = {},
24+
onLinkClick: (String) -> Unit = {},
2425
) {
2526
Popup(offset = IntOffset(0, -40)) {
2627
AnimatedVisibility(
@@ -37,6 +38,7 @@ fun TextToolbox(
3738
.padding(horizontal = 8.dp, vertical = 2.dp),
3839
iconSize = 28.dp,
3940
onSpanClick = onSpanClick,
41+
onLinkConfirm = onLinkClick,
4042
)
4143
}
4244
}

0 commit comments

Comments
 (0)