Skip to content

Commit

Permalink
Saving notes tutorial and fixing spans (#347)
Browse files Browse the repository at this point in the history
* Saving notes tutorial and fixing spans

* Update TextDrawer.kt

* Fixing spans

* cleaning code

* Simplifying test

---------

Co-authored-by: CI Bot <[email protected]>
  • Loading branch information
leandroBorgesFerreira and CI Bot authored Feb 19, 2025
1 parent 263e7f5 commit 63cab99
Show file tree
Hide file tree
Showing 16 changed files with 552 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ fun BoxScope.EditorScaffold(
modifier = Modifier
.align(Alignment.Center)
.let { modifierLet ->
if (maxWidth > 900.dp) {
modifierLet.width(1000.dp)
if (maxWidth > 750.dp) {
modifierLet.width(850.dp)
} else {
modifierLet.fillMaxWidth()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.writeopia.sdk.models.span.Span
import io.writeopia.sdk.models.span.SpanInfo
import io.writeopia.sdk.serialization.data.SpanInfoApi

fun SpanInfoApi.toModel() = SpanInfo(
fun SpanInfoApi.toModel() = SpanInfo.create(
start = this.start,
end = this.end,
span = Span.textFromString(this.span),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,64 @@
package io.writeopia.sdk.manager

import io.writeopia.sdk.models.id.GenerateId
import io.writeopia.sdk.models.span.Interception
import io.writeopia.sdk.models.span.Intersection
import io.writeopia.sdk.models.span.Span
import io.writeopia.sdk.models.span.SpanInfo
import io.writeopia.sdk.models.story.StoryStep
import kotlin.math.min

object SpansHandler {

fun toggleSpans(spanSet: Set<SpanInfo>, newSpan: SpanInfo): Set<SpanInfo> {
return when {
spanSet.contains(newSpan) -> spanSet - newSpan
spanSet.contains(newSpan) -> (spanSet - newSpan)

!spanSet.any { it.span == newSpan.span } -> spanSet + newSpan
!spanSet.any { it.span == newSpan.span } -> (spanSet + newSpan)

else -> {
val currentSpan = spanSet.first { it.span == newSpan.span }

val intersection: Interception = currentSpan.intersection(newSpan)
val intersection: Intersection = currentSpan.intersection(newSpan)

return when (intersection) {
Interception.INSIDE -> {
Intersection.CONTAINING -> {
val removed = (spanSet - currentSpan)

val currentStart = currentSpan.start
val currentEnd = currentSpan.end

val newStart = newSpan.start
val newEnd = newSpan.end

val splitSpans = setOf(
SpanInfo(
currentSpan.start,
newSpan.start,
SpanInfo.create(
currentStart,
newStart,
currentSpan.span
),
SpanInfo(
newSpan.end,
currentSpan.end,
SpanInfo.create(
newEnd,
currentEnd,
currentSpan.span
),
).filter { it.size() > 0 }

removed + splitSpans
}
Interception.INTERSECT -> {
val removed = (spanSet - currentSpan)
val minStart = min(currentSpan.start, newSpan.start)
val maxEnd = min(currentSpan.end, newSpan.end)

removed + SpanInfo(minStart, maxEnd, newSpan.span)
Intersection.INTERSECT -> {
val removed = (spanSet - currentSpan)
val expandedSpan = (currentSpan + newSpan)
removed + expandedSpan
}

Interception.OUTSIDE -> spanSet + newSpan
Intersection.OUTSIDE -> spanSet + newSpan

Interception.CONTAINING -> {
Intersection.INSIDE -> {
val removed = (spanSet - currentSpan)
removed + newSpan
}

Intersection.MATCH -> (spanSet - currentSpan)
}
}
}
Expand All @@ -71,7 +77,7 @@ object SpansHandler {
storySteps.mapValues { (_, story) ->
val text = story.text
if (text?.isNotEmpty() == true) {
val newSpanInfo = SpanInfo(0, text.length, newSpan)
val newSpanInfo = SpanInfo.create(0, text.length, newSpan)
story.copy(spans = story.spans + newSpanInfo, localId = GenerateId.generate())
} else {
story
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.writeopia.manager

import io.writeopia.sdk.manager.SpansHandler
import io.writeopia.sdk.models.span.Span
import io.writeopia.sdk.models.span.SpanInfo
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class SpansHandlerTest {

@Test
fun `it should be possible to add a span`() {
val boldSpan = SpanInfo.create(start = 0, end = 5, Span.BOLD)
val otherBoldSpan = SpanInfo.create(start = 7, end = 10, Span.BOLD)
val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), otherBoldSpan)

assertTrue { newSpans.contains(otherBoldSpan) }
}

@Test
fun `spans should be ordered before insertion`() {
val boldSpan = SpanInfo.create(start = 0, end = 5, Span.BOLD)
val otherBoldSpan = SpanInfo.create(start = 10, end = 7, Span.BOLD)
val expected = SpanInfo.create(start = 7, end = 10, Span.BOLD)

val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), otherBoldSpan)

assertTrue { newSpans.contains(expected) }
}

@Test
fun `it should be possible to remove a span`() {
val boldSpan = SpanInfo.create(start = 0, end = 5, Span.BOLD)
val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), boldSpan)

assertFalse { newSpans.contains(boldSpan) }
}

@Test
fun `it should be possible to expand a span`() {
val boldSpan = SpanInfo.create(start = 0, end = 5, Span.BOLD)
val boldSpan1 = SpanInfo.create(start = 3, end = 10, Span.BOLD)

val expected = SpanInfo.create(start = 0, end = 10, Span.BOLD)
val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), boldSpan1)

assertEquals(setOf(expected), newSpans)
}

@Test
fun `it should be possible to expand a span with inverted selection`() {
val boldSpan = SpanInfo.create(start = 5, end = 0, Span.BOLD)
val boldSpan1 = SpanInfo.create(start = 10, end = 3, Span.BOLD)

val expected = SpanInfo.create(start = 0, end = 10, Span.BOLD)
val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), boldSpan1)

assertEquals(setOf(expected), newSpans)
}

@Test
fun `adding a span inside another span should split it`() {
val boldSpan = SpanInfo.create(start = 0, end = 10, Span.BOLD)
val boldSpan1 = SpanInfo.create(start = 2, end = 8, Span.BOLD)

val expected = setOf(
SpanInfo.create(start = 0, end = 2, Span.BOLD),
SpanInfo.create(start = 8, end = 10, Span.BOLD)
)

val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), boldSpan1)

assertEquals(expected, newSpans)
}

@Test
fun `when adding different spans they should live together`() {
val boldSpan = SpanInfo.create(start = 0, end = 5, Span.BOLD)
val italicSpan = SpanInfo.create(start = 0, end = 5, Span.ITALIC)

val expected = setOf(boldSpan, italicSpan)
val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), italicSpan)

assertEquals(expected, newSpans)
}

@Test
fun `when adding a containing span only the new one should live`() {
val boldSpan = SpanInfo.create(start = 2, end = 10, Span.BOLD)
val boldSpan1 = SpanInfo.create(start = 0, end = 15, Span.BOLD)

val expected = setOf(boldSpan1)

val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), boldSpan1)

assertEquals(expected, newSpans)
}

@Test
fun `it should be possible to crop a span from end`() {
val boldSpan = SpanInfo.create(start = 0, end = 10, Span.BOLD)
val boldSpan1 = SpanInfo.create(start = 5, end = 10, Span.BOLD)

val expected = setOf(boldSpan.copy(end = 5))
val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), boldSpan1)

assertEquals(expected, newSpans)
}

@Test
fun `it should be possible to crop a span from start`() {
val boldSpan = SpanInfo.create(start = 0, end = 10, Span.BOLD)
val boldSpan1 = SpanInfo.create(start = 0, end = 5, Span.BOLD)

val expected = setOf(boldSpan.copy(start = 5))
val newSpans = SpansHandler.toggleSpans(setOf(boldSpan), boldSpan1)

assertEquals(expected, newSpans)
}
}
144 changes: 144 additions & 0 deletions writeopia_documents/Saving_Notes_EnxRIMrTMA.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
{
"id": "EnxRIMrTMA",
"title": "Saving Notes",
"userId": "disconnected_user",
"content": [
{
"id": "OiNhn5k3UO",
"type": {
"name": "title",
"number": 11
},
"text": "Saving Notes",
"decoration": {
"backgroundColor": -65281
},
"position": 0
},
{
"id": "ooAKXwwjqs",
"type": {
"name": "message",
"number": 0
},
"text": "Writeopia securely stores your notes within the app, but you also have the flexibility to choose a folder on your computer for saving and syncing your workspaces. This allows you to seamlessly back up your notes to your preferred cloud provider. With Writeopia, you’re in control of where your data lives!",
"position": 1
},
{
"id": "nbBnVFy4TP",
"type": {
"name": "message",
"number": 0
},
"text": "Configuring",
"tags": [
{
"tag": "H3",
"position": 0
}
],
"position": 2
},
{
"id": "HNwFfvfPzX",
"type": {
"name": "message",
"number": 0
},
"text": "Easily configure where your workspace is saved by selecting a folder on your computer. Go to Settings in the left sidebar and update the Local Folder section, or set it up when saving your workspace for the first time.",
"position": 3
},
{
"id": "S2sKPOe6ZV",
"type": {
"name": "message",
"number": 0
},
"text": "Saving workspace",
"tags": [
{
"tag": "H3",
"position": 0
}
],
"position": 4
},
{
"id": "fnz9CXLgzN",
"type": {
"name": "message",
"number": 0
},
"text": "Click the save icon in the top-right corner to save your workspace. To sync with notes in your local folder, use the sync icon. If a note exists in both places, the newest version will be kept and the older one will be updated. ",
"position": 5
},
{
"id": "aOdQquCaL3",
"type": {
"name": "message",
"number": 0
},
"text": "To save a single note, press CMD + SHIFT + S while inside the document to update it in your workspace. To save all notes at once, click the save icon in the notes menu.",
"position": 6
},
{
"id": "BVqFnUCkkt",
"type": {
"name": "message",
"number": 0
},
"text": "You can save your external files anywhere you prefer—an external hard drive, Dropbox, Google Drive, GitHub... the choice is yours!",
"position": 7
},
{
"id": "zBSwSrRhYq",
"type": {
"name": "message",
"number": 0
},
"text": "We're building our very own cloud service! In the future, you'll have the option to automatically save your data with us—but only if you choose to. Prefer full control? You can also self-host our service, keeping your data completely yours!",
"position": 8
},
{
"id": "TBw8hJkLhs",
"type": {
"name": "message",
"number": 0
},
"text": "File formats",
"tags": [
{
"tag": "H3",
"position": 0
}
],
"position": 9
},
{
"id": "QlH1ftsvrq",
"type": {
"name": "message",
"number": 0
},
"text": "With Writeopia, your notes are always yours! We save them in JSON instead of a proprietary format, and you can easily export them to Markdown. No lock-in—just complete freedom to use your data however you like!",
"position": 10
},
{
"id": "K4m1UPiik2",
"type": {
"name": "message",
"number": 0
},
"text": "",
"position": 12
}
],
"createdAt": 1739863082548,
"lastUpdatedAt": 1739880340633,
"parentId": "root",
"isLocked": false,
"icon": {
"label": "save",
"tint": -12303292
}
}
7 changes: 5 additions & 2 deletions writeopia_documents/Welcome!_816lZJxpi9.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,8 @@
"lastUpdatedAt": 1739783259799,
"parentId": "root",
"isLocked": false,
"icon": null
}
"icon": {
"label": "home",
"tint": -65281
}
}
2 changes: 1 addition & 1 deletion writeopia_documents/writeopia_config_file.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"lastUpdateTable": 1739783896101
"lastUpdateTable": 1739960766634
}
Loading

0 comments on commit 63cab99

Please sign in to comment.