From 9c7aa8cdc3622b55a0f01ead8d1e3e22b0dc92cb Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 11 Jul 2025 17:15:31 +0200 Subject: [PATCH 01/39] trying to sign intel --- .github/workflows/creates-desktop-apps.yml | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/creates-desktop-apps.yml b/.github/workflows/creates-desktop-apps.yml index eab4cceec..d2c27583a 100644 --- a/.github/workflows/creates-desktop-apps.yml +++ b/.github/workflows/creates-desktop-apps.yml @@ -83,4 +83,35 @@ jobs: with: name: Writeopia-macos path: application/composeApp/build/compose/binaries/main-release/dmg/Writeopia-1.0.0.dmg - + notarize_mac_os_intel: + name: Notarize Mac OS app + runs-on: macos-13 + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + architecture: 'x64' + - name: import certs + uses: apple-actions/import-codesign-certs@v3 + with: + p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} + p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} + - name: Generate signed mac app + run: ./gradlew packageReleaseDmg --no-daemon + env: + SIGN_IDENTITY: ${{ secrets.SIGN_IDENTITY }} + - name: Notarize + run: xcrun notarytool submit application/composeApp/build/compose/binaries/main-release/dmg/Writeopia-1.0.0.dmg --wait --apple-id $APPLE_ID_NOTARIZATION --password $NOTARIZATION_PASSWORD --team-id $APPSTORE_TEAM_ID + env: + APPLE_ID_NOTARIZATION: ${{ secrets.APPLE_ID_NOTARIZATION }} + APPSTORE_TEAM_ID: ${{ secrets.APPSTORE_TEAM_ID }} + NOTARIZATION_PASSWORD: ${{ secrets.NOTARIZATION_PASSWORD }} + - name: Upload App + uses: actions/upload-artifact@v4 + with: + name: Writeopia-macos + path: application/composeApp/build/compose/binaries/main-release/dmg/Writeopia-1.0.0.dmg From be7db33deb13a9a9b748113dd826b2b5bffa4af1 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 11 Jul 2025 17:16:49 +0200 Subject: [PATCH 02/39] name changes --- .github/workflows/creates-desktop-apps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/creates-desktop-apps.yml b/.github/workflows/creates-desktop-apps.yml index d2c27583a..779d15406 100644 --- a/.github/workflows/creates-desktop-apps.yml +++ b/.github/workflows/creates-desktop-apps.yml @@ -84,7 +84,7 @@ jobs: name: Writeopia-macos path: application/composeApp/build/compose/binaries/main-release/dmg/Writeopia-1.0.0.dmg notarize_mac_os_intel: - name: Notarize Mac OS app + name: Notarize Mac OS app - Intel runs-on: macos-13 steps: - name: Check out code @@ -113,5 +113,5 @@ jobs: - name: Upload App uses: actions/upload-artifact@v4 with: - name: Writeopia-macos + name: Writeopia-macos-intel path: application/composeApp/build/compose/binaries/main-release/dmg/Writeopia-1.0.0.dmg From e1ae277a018ca2f5d95390249c9777ed40ed4739 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 11 Jul 2025 17:40:17 +0200 Subject: [PATCH 03/39] Update creates-desktop-apps.yml --- .github/workflows/creates-desktop-apps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/creates-desktop-apps.yml b/.github/workflows/creates-desktop-apps.yml index 779d15406..567571927 100644 --- a/.github/workflows/creates-desktop-apps.yml +++ b/.github/workflows/creates-desktop-apps.yml @@ -101,7 +101,7 @@ jobs: p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} - name: Generate signed mac app - run: ./gradlew packageReleaseDmg --no-daemon + run: ./gradlew packageReleaseDmg -Pcompose.desktop.macos.arch=x64 --no-daemon env: SIGN_IDENTITY: ${{ secrets.SIGN_IDENTITY }} - name: Notarize From 0bd87950a23d138736611d3aae95478458a5b9ee Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 11 Jul 2025 17:44:20 +0200 Subject: [PATCH 04/39] Update creates-desktop-apps.yml --- .github/workflows/creates-desktop-apps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/creates-desktop-apps.yml b/.github/workflows/creates-desktop-apps.yml index 567571927..6ff4e9f35 100644 --- a/.github/workflows/creates-desktop-apps.yml +++ b/.github/workflows/creates-desktop-apps.yml @@ -94,7 +94,7 @@ jobs: with: distribution: 'zulu' java-version: '17' - architecture: 'x64' + architecture: 'x86' - name: import certs uses: apple-actions/import-codesign-certs@v3 with: From 9509635f69a8c703377e7f906d437a667e7f9865 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 11 Jul 2025 17:50:59 +0200 Subject: [PATCH 05/39] Update creates-desktop-apps.yml --- .github/workflows/creates-desktop-apps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/creates-desktop-apps.yml b/.github/workflows/creates-desktop-apps.yml index 6ff4e9f35..1f3cbd7e2 100644 --- a/.github/workflows/creates-desktop-apps.yml +++ b/.github/workflows/creates-desktop-apps.yml @@ -92,9 +92,9 @@ jobs: - name: Set up JDK 21 uses: actions/setup-java@v4 with: - distribution: 'zulu' + distribution: 'temurin' java-version: '17' - architecture: 'x86' + architecture: 'x64' - name: import certs uses: apple-actions/import-codesign-certs@v3 with: From 056920d31882782739575caf30b27899c68938cb Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Sun, 13 Jul 2025 23:11:11 +0200 Subject: [PATCH 06/39] Changing video of documentation --- .../docs/application/move-documents/move-documents.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/writeopia_docs/docs/application/move-documents/move-documents.mdx b/documentation/writeopia_docs/docs/application/move-documents/move-documents.mdx index 4871e003a..c187b66a9 100644 --- a/documentation/writeopia_docs/docs/application/move-documents/move-documents.mdx +++ b/documentation/writeopia_docs/docs/application/move-documents/move-documents.mdx @@ -10,7 +10,7 @@ You can place documents inside folders, and you can also nest folders within oth From de9fee1a44866225266be56c611482c160625f6f Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Sun, 13 Jul 2025 23:18:00 +0200 Subject: [PATCH 07/39] Fixing videos for mobile --- .../docs/application/commands/commands.mdx | 6 +++--- .../docs/application/importing/importing.mdx | 6 ++---- .../docs/application/ui-commands/ui-commands.mdx | 9 +++++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/documentation/writeopia_docs/docs/application/commands/commands.mdx b/documentation/writeopia_docs/docs/application/commands/commands.mdx index 2ecb0f449..e723896dd 100644 --- a/documentation/writeopia_docs/docs/application/commands/commands.mdx +++ b/documentation/writeopia_docs/docs/application/commands/commands.mdx @@ -9,12 +9,12 @@ Writeopia handles many text commands that create new content from what the user ## Checkbox Typing the command `-[] ` or `[] ` creates a checkbox in the place of current paragraph. The command must be done at the beginning of the line. - + ## List item Typing the command `- ` creates a list item in the place of current paragraph. The command must be done at the beginning of the line. - + ## H1, H2, H3, H4 Writeopia also has commands to create H1, H2, H3 and H4. @@ -28,7 +28,7 @@ Writeopia also has commands to create H1, H2, H3 and H4. H3 sample: - +
diff --git a/documentation/writeopia_docs/docs/application/importing/importing.mdx b/documentation/writeopia_docs/docs/application/importing/importing.mdx index 4bee20cb6..2fc1952d1 100644 --- a/documentation/writeopia_docs/docs/application/importing/importing.mdx +++ b/documentation/writeopia_docs/docs/application/importing/importing.mdx @@ -13,13 +13,13 @@ You can drag and drop your files into Writeopia to create notes. ### Json Notes You can drop your notes to import them. The json must be the format of the notes of Writeopia. - +
### Images You can also import images to create an image note. Simply drag your images into Writeopia to document them. - +
### Markdown @@ -32,5 +32,3 @@ You can also drag and drop markdown files. controls url="https://storage.googleapis.com/writeopia-resources/docusaurus/import_document.mov" width="100%" height="600px" /> - - diff --git a/documentation/writeopia_docs/docs/application/ui-commands/ui-commands.mdx b/documentation/writeopia_docs/docs/application/ui-commands/ui-commands.mdx index bded81b25..14fd25d50 100644 --- a/documentation/writeopia_docs/docs/application/ui-commands/ui-commands.mdx +++ b/documentation/writeopia_docs/docs/application/ui-commands/ui-commands.mdx @@ -10,7 +10,7 @@ This sections explains the actions that Writeopia provides by clicking in the bu ## Select text There are 3 ways to select text. It is possible to horizontally swipe the text, click in the drag button or click in the text after pressing Crtl + Alt (Windows/Linux) or Ctrl + CMD (macOS). - +
Once multiple paragraphs are selection, many bulk actions can be done, like deleting many lines at once, moving many at once or making a AI question that includes many lines. @@ -18,14 +18,14 @@ Once multiple paragraphs are selection, many bulk actions can be done, like dele ## Drag and Drop The way to move content is to use the drag icon on the right side of it. Dragging the icon will trigger the drag and drop action. - +
## Undo/Redo **Android:** By clicking in the arrows in the bottom of the editor, you can undo actions. **Web and desktop:** By pressing cmd+z (macos) or ctrl+z (windows/linux), , you can undo actions. - + ## Text selected - Mobile When text is selected, new options of commands become available to interact with the selected text. @@ -33,4 +33,5 @@ When text is selected, new options of commands become available to interact with + height="600px" + width="100%"/> From 05838a9e2634907a7872b5424d6b6ceb137ea86f Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Mon, 14 Jul 2025 10:40:21 +0200 Subject: [PATCH 08/39] fixing kdocs --- .../sdk/import/files/KmpFileReader.native.kt | 3 +- .../target/external/HandleImageDrop.native.kt | 2 +- .../ui/drawer/factory/DefaultDrawersNative.kt | 56 +++++++++++++++---- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/plugins/writeopia_import_document/src/nativeMain/kotlin/io/writeopia/sdk/import/files/KmpFileReader.native.kt b/plugins/writeopia_import_document/src/nativeMain/kotlin/io/writeopia/sdk/import/files/KmpFileReader.native.kt index 1f6518950..3881ecc2a 100644 --- a/plugins/writeopia_import_document/src/nativeMain/kotlin/io/writeopia/sdk/import/files/KmpFileReader.native.kt +++ b/plugins/writeopia_import_document/src/nativeMain/kotlin/io/writeopia/sdk/import/files/KmpFileReader.native.kt @@ -20,7 +20,8 @@ actual object KmpFileReader { actual inline fun readDirectory( directoryPath: String, - json: Json + json: Json, + crossinline predicate: (String) -> Boolean ): Flow { TODO("Not yet implemented") } diff --git a/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/draganddrop/target/external/HandleImageDrop.native.kt b/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/draganddrop/target/external/HandleImageDrop.native.kt index ac13b3d71..15b91fcb9 100644 --- a/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/draganddrop/target/external/HandleImageDrop.native.kt +++ b/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/draganddrop/target/external/HandleImageDrop.native.kt @@ -8,4 +8,4 @@ actual fun handleImageDrop( receiveFiles: (List) -> Unit ): Boolean = false -fun shouldAcceptImageDrop(event: DragAndDropEvent): Boolean = false +actual fun shouldAcceptImageDrop(event: DragAndDropEvent): Boolean = false diff --git a/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt b/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt index 009b0dbc6..45d97fb37 100644 --- a/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt +++ b/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt @@ -1,11 +1,22 @@ package io.writeopia.ui.drawer.factory +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp +import io.writeopia.sdk.models.files.ExternalFile +import io.writeopia.sdk.models.story.StoryTypes +import io.writeopia.sdk.models.story.Tag import io.writeopia.ui.drawer.StoryStepDrawer +import io.writeopia.ui.icons.WrSdkIcons import io.writeopia.ui.manager.WriteopiaStateManager import io.writeopia.ui.model.DrawConfig @@ -14,36 +25,59 @@ object DefaultDrawersNative : DrawersFactory { @Composable override fun create( manager: WriteopiaStateManager, - defaultBorder: Shape, editable: Boolean, - groupsBackgroundColor: Color, + aiExplanation: String, + isDarkTheme: Boolean, onHeaderClick: () -> Unit, drawConfig: DrawConfig, fontFamily: FontFamily?, + generateSection: (Int) -> Unit, + receiveExternalFile: (List, Int) -> Unit, onDocumentLinkClick: (String) -> Unit, ): Map { val commonDrawers = CommonDrawers.create( manager, marginAtBottom = 500.dp, - defaultBorder, + aiExplanation = aiExplanation, editable, onHeaderClick, lineBreakByContent = true, - drawConfig = drawConfig, isDesktop = false, + isDarkTheme = isDarkTheme, + drawConfig = drawConfig, eventListener = KeyEventListenerFactory.android( manager, isEmptyErase = { _, _ -> false } ), - onDocumentLinkClick = onDocumentLinkClick + fontFamily = fontFamily, + headerEndContent = { storyStep, drawInfo, _ -> + val isTitle = storyStep.tags.any { it.tag.isTitle() } + val isCollapsed by lazy { storyStep.tags.any { it.tag == Tag.COLLAPSED } } + if (isTitle) { + val iconTintOnHover = MaterialTheme.colorScheme.onBackground + + Icon( + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .combinedClickable( + onClick = { manager.toggleCollapseItem(drawInfo.position) }, + onLongClick = { manager.onSectionSelected(drawInfo.position) } + ) + .size(24.dp) + .padding(4.dp), + imageVector = if (isCollapsed) { + WrSdkIcons.smallArrowUp + } else { + WrSdkIcons.smallArrowDown + }, + contentDescription = "Small arrow right", + tint = iconTintOnHover + ) + } + }, + onDocumentLinkClick = onDocumentLinkClick, ) return commonDrawers } - -// private fun emptyErase( -// keyEvent: androidx.compose.ui.input.key.KeyEvent, -// input: TextFieldValue -// ): Boolean = -// keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL && input.selection.start == 0 } From fba893e658936addc6b12a3ae540b27e6627eb7d Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Mon, 14 Jul 2025 10:53:19 +0200 Subject: [PATCH 09/39] Fixing urls or kdocs and javadocs --- .../writeopia_docs/docs/sdk/api_reference.mdx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/documentation/writeopia_docs/docs/sdk/api_reference.mdx b/documentation/writeopia_docs/docs/sdk/api_reference.mdx index a84d568f4..a1219e3b1 100644 --- a/documentation/writeopia_docs/docs/sdk/api_reference.mdx +++ b/documentation/writeopia_docs/docs/sdk/api_reference.mdx @@ -4,17 +4,21 @@ The documentation is provided both in KFocs and Javadocs. You can choose the doc ## Kdocs -You can check the generated documentation with [KDocs](https://kdocs.writeopia.io/) +You can check the generated documentation with [KDocs](https://writeopia.github.io/WriteopiaKdocs/) ## Javadocs You can check the Javadocs for each module. ### Core components -- [Writeopia](https://javadoc.io/doc/com.github.leandroborgesferreira/storyteller) -- [Writeopia Models](https://javadoc.io/doc/com.github.leandroborgesferreira/storyteller-models) +- [Writeopia](https://javadoc.io/doc/io.writeopia/writeopia-core/latest/index.html) +- [Writeopia Models](https://javadoc.io/doc/io.writeopia/writeopia-models/latest/index.html) ### Plugins -- [Writeopia Network](https://javadoc.io/doc/com.github.leandroborgesferreira/storyteller-network) -- [Writeopia Persistence](https://javadoc.io/doc/com.github.leandroborgesferreira/storyteller-persistence) -- [Writeopia Serialization](https://javadoc.io/doc/com.github.leandroborgesferreira/storyteller-serialization) +- [Writeopia Network](https://javadoc.io/doc/io.writeopia/writeopia-network/latest/index.html) +- [Writeopia Persistence](https://javadoc.io/doc/io.writeopia/writeopia-persistence-core/latest/index.html) +- [Writeopia Persistence Room](https://javadoc.io/doc/io.writeopia/writeopia-persistence-room/latest/index.html) +- [Writeopia Persistence SqlDelight](https://javadoc.io/doc/io.writeopia/writeopia-persistence-sqldelight/latest/index.html) +- [Writeopia Serialization](https://javadoc.io/doc/io.writeopia/writeopia-serialization/latest/index.html) +- [Writeopia Export](https://javadoc.io/doc/io.writeopia/writeopia-export/latest/index.html) +- [Writeopia Import](https://javadoc.io/doc/io.writeopia/writeopia-import/latest/index.html) From 699af027fbf28223fff0fd4f643140f462d059a8 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Tue, 15 Jul 2025 11:40:02 +0200 Subject: [PATCH 10/39] Adding javadoc for writeopia ui --- documentation/writeopia_docs/docs/sdk/api_reference.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/writeopia_docs/docs/sdk/api_reference.mdx b/documentation/writeopia_docs/docs/sdk/api_reference.mdx index a1219e3b1..590a08894 100644 --- a/documentation/writeopia_docs/docs/sdk/api_reference.mdx +++ b/documentation/writeopia_docs/docs/sdk/api_reference.mdx @@ -11,8 +11,9 @@ You can check the generated documentation with [KDocs](https://writeopia.github. You can check the Javadocs for each module. ### Core components -- [Writeopia](https://javadoc.io/doc/io.writeopia/writeopia-core/latest/index.html) - [Writeopia Models](https://javadoc.io/doc/io.writeopia/writeopia-models/latest/index.html) +- [Writeopia Core](https://javadoc.io/doc/io.writeopia/writeopia-core/latest/index.html) +- [Writeopia UI](https://javadoc.io/doc/io.writeopia/writeopia-ui/latest/index.html) ### Plugins - [Writeopia Network](https://javadoc.io/doc/io.writeopia/writeopia-network/latest/index.html) From 72139e4efe8b9eee8f9686ec3c506ff290479de9 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Sat, 19 Jul 2025 11:42:27 +0200 Subject: [PATCH 11/39] Adding MIT license for SDK --- plugins/writeopia_export/LICENSE-MIT.md | 21 +++++++++++++++++++ .../writeopia_import_document/LICENSE-MIT.md | 21 +++++++++++++++++++ plugins/writeopia_network/LICENSE-MIT.md | 21 +++++++++++++++++++ .../writeopia_persistence_core/LICENSE-MIT.md | 21 +++++++++++++++++++ .../writeopia_persistence_room/LICENSE-MIT.md | 21 +++++++++++++++++++ .../LICENSE-MIT.md | 21 +++++++++++++++++++ plugins/writeopia_presentation/LICENSE-MIT.md | 21 +++++++++++++++++++ .../writeopia_serialization/LICENSE-MIT.md | 21 +++++++++++++++++++ writeopia/LICENSE-MIT.md | 21 +++++++++++++++++++ writeopia_models/LICENSE-MIT.md | 21 +++++++++++++++++++ writeopia_ui/LICENSE-MIT.md | 21 +++++++++++++++++++ 11 files changed, 231 insertions(+) create mode 100644 plugins/writeopia_export/LICENSE-MIT.md create mode 100644 plugins/writeopia_import_document/LICENSE-MIT.md create mode 100644 plugins/writeopia_network/LICENSE-MIT.md create mode 100644 plugins/writeopia_persistence_core/LICENSE-MIT.md create mode 100644 plugins/writeopia_persistence_room/LICENSE-MIT.md create mode 100644 plugins/writeopia_persistence_sqldelight/LICENSE-MIT.md create mode 100644 plugins/writeopia_presentation/LICENSE-MIT.md create mode 100644 plugins/writeopia_serialization/LICENSE-MIT.md create mode 100644 writeopia/LICENSE-MIT.md create mode 100644 writeopia_models/LICENSE-MIT.md create mode 100644 writeopia_ui/LICENSE-MIT.md diff --git a/plugins/writeopia_export/LICENSE-MIT.md b/plugins/writeopia_export/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_export/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/writeopia_import_document/LICENSE-MIT.md b/plugins/writeopia_import_document/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_import_document/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/writeopia_network/LICENSE-MIT.md b/plugins/writeopia_network/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_network/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/writeopia_persistence_core/LICENSE-MIT.md b/plugins/writeopia_persistence_core/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_persistence_core/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/writeopia_persistence_room/LICENSE-MIT.md b/plugins/writeopia_persistence_room/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_persistence_room/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/writeopia_persistence_sqldelight/LICENSE-MIT.md b/plugins/writeopia_persistence_sqldelight/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_persistence_sqldelight/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/writeopia_presentation/LICENSE-MIT.md b/plugins/writeopia_presentation/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_presentation/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/writeopia_serialization/LICENSE-MIT.md b/plugins/writeopia_serialization/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/plugins/writeopia_serialization/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/writeopia/LICENSE-MIT.md b/writeopia/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/writeopia/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/writeopia_models/LICENSE-MIT.md b/writeopia_models/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/writeopia_models/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/writeopia_ui/LICENSE-MIT.md b/writeopia_ui/LICENSE-MIT.md new file mode 100644 index 000000000..64def88bc --- /dev/null +++ b/writeopia_ui/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Writeopia Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From b53595db601d841c81e4ac530ed258501f7b2d4c Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Sat, 19 Jul 2025 11:43:39 +0200 Subject: [PATCH 12/39] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 72fcbaf11..eb17d38ea 100644 --- a/README.md +++ b/README.md @@ -74,4 +74,6 @@ To start the android app use Android Studio. # SDK The SDK allows you to create a text editor an use it instead of a simple `TextField`. You can use this library to create a better story editing experience within apps, such as social media (posts), travel logs, blog apps (like Medium), and chats (like Slack). By using this library, you can provide a great experience for editing text with media. +The SDK is under MIT license you can freely use it. + You can more in the [SDK documentation](https://docs.writeopia.io/docs/sdk/overview) From ea9ff83dc287615090b8b6fd961c8a4323e2218f Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Mon, 28 Jul 2025 21:21:18 +0200 Subject: [PATCH 13/39] Equation drawer (#492) * Sketch of equation drawer * fixing ktlint * Polishing equation editor * sdk version bump * code clean --------- Co-authored-by: Leandro Ferreira --- .../wide_screen_common/build.gradle.kts | 2 + application/composeApp/build.gradle.kts | 3 + .../io/writeopia/desktop/MainDesktop.kt | 4 + application/core/auth_core/build.gradle.kts | 2 + application/core/common_ui/build.gradle.kts | 2 + .../core/common_ui_tests/build.gradle.kts | 2 + .../core/configuration/build.gradle.kts | 2 + application/core/connection/build.gradle.kts | 2 + application/core/documents/build.gradle.kts | 2 + application/core/forcegraph/build.gradle.kts | 2 + application/core/models/build.gradle.kts | 2 + application/core/navigation/build.gradle.kts | 2 + application/core/ollama/build.gradle.kts | 2 + .../core/persistence_bridge/build.gradle.kts | 2 + .../core/persistence_room/build.gradle.kts | 2 + .../persistence_sqldelight/build.gradle.kts | 2 + application/core/resources/build.gradle.kts | 2 + application/core/theme/build.gradle.kts | 2 + application/core/utils/build.gradle.kts | 2 + .../common/utils/keyboard/KeyboardCommands.kt | 4 + application/features/account/build.gradle.kts | 2 + application/features/auth/build.gradle.kts | 2 + .../features/documents_graph/build.gradle.kts | 2 + application/features/editor/build.gradle.kts | 2 + .../editor/features/editor/ui/TextEditor.kt | 1 + .../features/global_shell/build.gradle.kts | 2 + .../features/note_menu/build.gradle.kts | 2 + .../features/notifications/build.gradle.kts | 2 + .../features/onboarding/build.gradle.kts | 2 + application/features/search/build.gradle.kts | 2 + application/web/build.gradle.kts | 4 + build.gradle.kts | 2 +- common/endpoints/build.gradle.kts | 2 + gradle/libs.versions.toml | 2 +- libraries/dbtest/build.gradle.kts | 2 + plugins/writeopia_export/build.gradle.kts | 2 + .../build.gradle.kts | 2 + plugins/writeopia_network/build.gradle.kts | 2 + .../build.gradle.kts | 2 + .../build.gradle.kts | 2 + .../build.gradle.kts | 2 + .../writeopia_presentation/build.gradle.kts | 2 + .../writeopia_serialization/build.gradle.kts | 2 + tutorials/build.gradle.kts | 2 + writeopia/build.gradle.kts | 2 + .../sdk/repository/DocumentRepository.kt | 1 + writeopia_models/build.gradle.kts | 2 + .../writeopia/sdk/models/story/StoryTypes.kt | 1 + writeopia_ui/build.gradle.kts | 2 + .../drawer/factory/DefaultDrawersAndroid.kt | 2 + .../ui/drawer/content/EquationDrawer.kt | 187 ++++++++++++++++++ .../ui/drawer/factory/CommonDrawers.kt | 18 ++ .../ui/drawer/factory/DrawersFactory.kt | 1 + .../io/writeopia/ui/icons/WrSdkIcons.kt | 3 + .../io/writeopia/ui/icons/all/Pencil.kt | 55 ++++++ .../io/writeopia/ui/keyboard/KeyboardEvent.kt | 1 + .../ui/manager/WriteopiaStateManager.kt | 32 ++- .../ui/drawer/factory/DefaultDrawersJs.kt | 1 + .../drawer/factory/DefaultDrawersDesktop.kt | 2 + .../ui/drawer/factory/DefaultDrawersNative.kt | 5 +- 60 files changed, 403 insertions(+), 6 deletions(-) create mode 100644 writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt create mode 100644 writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/all/Pencil.kt diff --git a/application/common_flows/wide_screen_common/build.gradle.kts b/application/common_flows/wide_screen_common/build.gradle.kts index 7541ccc97..55bca2146 100644 --- a/application/common_flows/wide_screen_common/build.gradle.kts +++ b/application/common_flows/wide_screen_common/build.gradle.kts @@ -8,6 +8,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm() js(IR) { browser() diff --git a/application/composeApp/build.gradle.kts b/application/composeApp/build.gradle.kts index c0d8ef935..e21070846 100644 --- a/application/composeApp/build.gradle.kts +++ b/application/composeApp/build.gradle.kts @@ -10,6 +10,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() @@ -66,6 +68,7 @@ kotlin { implementation(libs.compose.navigation) implementation(libs.platformtools.darkmodedetector) + implementation(libs.coil.compose) } } diff --git a/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt b/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt index c67e97a1c..7d231acb6 100644 --- a/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt +++ b/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt @@ -169,6 +169,10 @@ private fun ApplicationScope.App(onCloseRequest: () -> Unit = ::exitApplication) sendEvent(KeyboardEvent.REDO) false } + KeyboardCommands.isEquationEvent(keyEvent) -> { + sendEvent(KeyboardEvent.EQUATION) + false + } else -> false } diff --git a/application/core/auth_core/build.gradle.kts b/application/core/auth_core/build.gradle.kts index b44826d42..1b5f57924 100644 --- a/application/core/auth_core/build.gradle.kts +++ b/application/core/auth_core/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/common_ui/build.gradle.kts b/application/core/common_ui/build.gradle.kts index 5d41bf294..29a939388 100644 --- a/application/core/common_ui/build.gradle.kts +++ b/application/core/common_ui/build.gradle.kts @@ -6,6 +6,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/common_ui_tests/build.gradle.kts b/application/core/common_ui_tests/build.gradle.kts index f93f66500..ca010f21f 100644 --- a/application/core/common_ui_tests/build.gradle.kts +++ b/application/core/common_ui_tests/build.gradle.kts @@ -8,6 +8,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} sourceSets { val commonMain by getting { diff --git a/application/core/configuration/build.gradle.kts b/application/core/configuration/build.gradle.kts index 535dd3526..1b1c5eb7f 100644 --- a/application/core/configuration/build.gradle.kts +++ b/application/core/configuration/build.gradle.kts @@ -4,6 +4,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/connection/build.gradle.kts b/application/core/connection/build.gradle.kts index e0628b3f1..b351af36f 100644 --- a/application/core/connection/build.gradle.kts +++ b/application/core/connection/build.gradle.kts @@ -4,6 +4,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/documents/build.gradle.kts b/application/core/documents/build.gradle.kts index 07bf91c67..98d7bc1ea 100644 --- a/application/core/documents/build.gradle.kts +++ b/application/core/documents/build.gradle.kts @@ -6,6 +6,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/forcegraph/build.gradle.kts b/application/core/forcegraph/build.gradle.kts index 284a5a390..8b10c670c 100644 --- a/application/core/forcegraph/build.gradle.kts +++ b/application/core/forcegraph/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/core/models/build.gradle.kts b/application/core/models/build.gradle.kts index 10b0bb40b..f14e4ad62 100644 --- a/application/core/models/build.gradle.kts +++ b/application/core/models/build.gradle.kts @@ -4,6 +4,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/application/core/navigation/build.gradle.kts b/application/core/navigation/build.gradle.kts index 73fdcbcff..ee1cae641 100644 --- a/application/core/navigation/build.gradle.kts +++ b/application/core/navigation/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm {} diff --git a/application/core/ollama/build.gradle.kts b/application/core/ollama/build.gradle.kts index 4ab3fbf4b..888e772a6 100644 --- a/application/core/ollama/build.gradle.kts +++ b/application/core/ollama/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/application/core/persistence_bridge/build.gradle.kts b/application/core/persistence_bridge/build.gradle.kts index a1a413f38..4295e04cb 100644 --- a/application/core/persistence_bridge/build.gradle.kts +++ b/application/core/persistence_bridge/build.gradle.kts @@ -5,6 +5,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/persistence_room/build.gradle.kts b/application/core/persistence_room/build.gradle.kts index 3fe72ad74..ef432e7be 100644 --- a/application/core/persistence_room/build.gradle.kts +++ b/application/core/persistence_room/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/persistence_sqldelight/build.gradle.kts b/application/core/persistence_sqldelight/build.gradle.kts index bfe23cbf4..ca4bfec3a 100644 --- a/application/core/persistence_sqldelight/build.gradle.kts +++ b/application/core/persistence_sqldelight/build.gradle.kts @@ -4,6 +4,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/application/core/resources/build.gradle.kts b/application/core/resources/build.gradle.kts index 907702273..fd250fb21 100644 --- a/application/core/resources/build.gradle.kts +++ b/application/core/resources/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/theme/build.gradle.kts b/application/core/theme/build.gradle.kts index e44987b67..31c46f5a7 100644 --- a/application/core/theme/build.gradle.kts +++ b/application/core/theme/build.gradle.kts @@ -9,6 +9,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/utils/build.gradle.kts b/application/core/utils/build.gradle.kts index 9aa1d90b1..444d2ff10 100644 --- a/application/core/utils/build.gradle.kts +++ b/application/core/utils/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt index 7c1bfb883..1d585ca3d 100644 --- a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt +++ b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt @@ -83,4 +83,8 @@ object KeyboardCommands { fun isAcceptAiEvent(keyEvent: KeyEvent) = keyEvent.key.keyCode == Key.Tab.keyCode && keyEvent.type == KeyEventType.KeyUp + + fun isEquationEvent(keyEvent: KeyEvent) = + keyEvent.key.keyCode == Key.E.keyCode && + keyEvent.type == KeyEventType.KeyUp } diff --git a/application/features/account/build.gradle.kts b/application/features/account/build.gradle.kts index 4b6c844ce..6f5b53e51 100644 --- a/application/features/account/build.gradle.kts +++ b/application/features/account/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm {} diff --git a/application/features/auth/build.gradle.kts b/application/features/auth/build.gradle.kts index cca5ac041..f56850cda 100644 --- a/application/features/auth/build.gradle.kts +++ b/application/features/auth/build.gradle.kts @@ -9,6 +9,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/features/documents_graph/build.gradle.kts b/application/features/documents_graph/build.gradle.kts index 5b9fdba41..1633f3624 100644 --- a/application/features/documents_graph/build.gradle.kts +++ b/application/features/documents_graph/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/features/editor/build.gradle.kts b/application/features/editor/build.gradle.kts index 9f6bac1bb..518444443 100644 --- a/application/features/editor/build.gradle.kts +++ b/application/features/editor/build.gradle.kts @@ -8,6 +8,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt index 5374fcd87..2a96483cf 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt @@ -73,6 +73,7 @@ internal fun TextEditor( generateSection = noteEditorViewModel::aiSection, receiveExternalFile = noteEditorViewModel::receiveExternalFile, onDocumentLinkClick = onDocumentLinkClick, + equationToImageUrl = "https://latex.codecogs.com/png.latex?\\Large&space;x=" ), storyState = storyState, ) diff --git a/application/features/global_shell/build.gradle.kts b/application/features/global_shell/build.gradle.kts index 9d8b2c3be..01e593f2b 100644 --- a/application/features/global_shell/build.gradle.kts +++ b/application/features/global_shell/build.gradle.kts @@ -8,6 +8,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/features/note_menu/build.gradle.kts b/application/features/note_menu/build.gradle.kts index 5b3ac9367..4f7bb720d 100644 --- a/application/features/note_menu/build.gradle.kts +++ b/application/features/note_menu/build.gradle.kts @@ -7,6 +7,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/features/notifications/build.gradle.kts b/application/features/notifications/build.gradle.kts index 0cc8e3036..816e6cf75 100644 --- a/application/features/notifications/build.gradle.kts +++ b/application/features/notifications/build.gradle.kts @@ -8,6 +8,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/features/onboarding/build.gradle.kts b/application/features/onboarding/build.gradle.kts index a947abca3..7b629ec66 100644 --- a/application/features/onboarding/build.gradle.kts +++ b/application/features/onboarding/build.gradle.kts @@ -6,6 +6,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/application/features/search/build.gradle.kts b/application/features/search/build.gradle.kts index dbf97908a..8ff408372 100644 --- a/application/features/search/build.gradle.kts +++ b/application/features/search/build.gradle.kts @@ -8,6 +8,8 @@ plugins { } kotlin { + jvmToolchain(21) + androidTarget() jvm() diff --git a/application/web/build.gradle.kts b/application/web/build.gradle.kts index 4cab224e7..b472ffa58 100644 --- a/application/web/build.gradle.kts +++ b/application/web/build.gradle.kts @@ -6,6 +6,10 @@ plugins { } kotlin { + jvmToolchain(21) + + jvmToolchain(21) + js(IR) { browser() binaries.executable() diff --git a/build.gradle.kts b/build.gradle.kts index 0683d05c1..5fc416ba6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,4 +21,4 @@ dagCommand { printModulesInfo = true } -extra["sdkVersion"] = "0.11.1" +extra["sdkVersion"] = "0.12.0" diff --git a/common/endpoints/build.gradle.kts b/common/endpoints/build.gradle.kts index 4ab5a53d0..d13816281 100644 --- a/common/endpoints/build.gradle.kts +++ b/common/endpoints/build.gradle.kts @@ -5,6 +5,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3716cc2bd..fd5efdc6c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ ktx = "1.16.0" material = "1.12.0" platformtools = "0.4.0" room = "2.7.2" -agp = "8.11.0" +agp = "8.11.1" kotlin = "2.2.0" composeTest = "1.8.3" webWorkerDriver = "2.1.0" diff --git a/libraries/dbtest/build.gradle.kts b/libraries/dbtest/build.gradle.kts index bf88402f2..821fb0c80 100644 --- a/libraries/dbtest/build.gradle.kts +++ b/libraries/dbtest/build.gradle.kts @@ -4,6 +4,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/plugins/writeopia_export/build.gradle.kts b/plugins/writeopia_export/build.gradle.kts index bfce3416f..6ad4e1d65 100644 --- a/plugins/writeopia_export/build.gradle.kts +++ b/plugins/writeopia_export/build.gradle.kts @@ -51,6 +51,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/plugins/writeopia_import_document/build.gradle.kts b/plugins/writeopia_import_document/build.gradle.kts index 0a19f2043..d22bb1cbe 100644 --- a/plugins/writeopia_import_document/build.gradle.kts +++ b/plugins/writeopia_import_document/build.gradle.kts @@ -51,6 +51,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/plugins/writeopia_network/build.gradle.kts b/plugins/writeopia_network/build.gradle.kts index 3c3db3767..fdb024f3d 100644 --- a/plugins/writeopia_network/build.gradle.kts +++ b/plugins/writeopia_network/build.gradle.kts @@ -53,6 +53,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/plugins/writeopia_persistence_core/build.gradle.kts b/plugins/writeopia_persistence_core/build.gradle.kts index 9429d1c73..81583decc 100644 --- a/plugins/writeopia_persistence_core/build.gradle.kts +++ b/plugins/writeopia_persistence_core/build.gradle.kts @@ -51,6 +51,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/plugins/writeopia_persistence_room/build.gradle.kts b/plugins/writeopia_persistence_room/build.gradle.kts index 5388258e8..9a8f3f601 100644 --- a/plugins/writeopia_persistence_room/build.gradle.kts +++ b/plugins/writeopia_persistence_room/build.gradle.kts @@ -81,6 +81,8 @@ android { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/plugins/writeopia_persistence_sqldelight/build.gradle.kts b/plugins/writeopia_persistence_sqldelight/build.gradle.kts index 5ee640eb8..0bf931fd9 100644 --- a/plugins/writeopia_persistence_sqldelight/build.gradle.kts +++ b/plugins/writeopia_persistence_sqldelight/build.gradle.kts @@ -50,6 +50,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/plugins/writeopia_presentation/build.gradle.kts b/plugins/writeopia_presentation/build.gradle.kts index d9b3110d6..67ebd3ce4 100644 --- a/plugins/writeopia_presentation/build.gradle.kts +++ b/plugins/writeopia_presentation/build.gradle.kts @@ -52,6 +52,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/plugins/writeopia_serialization/build.gradle.kts b/plugins/writeopia_serialization/build.gradle.kts index 9e77883f4..b9d0d4892 100644 --- a/plugins/writeopia_serialization/build.gradle.kts +++ b/plugins/writeopia_serialization/build.gradle.kts @@ -51,6 +51,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/tutorials/build.gradle.kts b/tutorials/build.gradle.kts index 411b8c60e..28b0ef4d7 100644 --- a/tutorials/build.gradle.kts +++ b/tutorials/build.gradle.kts @@ -6,6 +6,8 @@ plugins { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/writeopia/build.gradle.kts b/writeopia/build.gradle.kts index 78e4c9904..a287886de 100644 --- a/writeopia/build.gradle.kts +++ b/writeopia/build.gradle.kts @@ -50,6 +50,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/writeopia/src/commonMain/kotlin/io/writeopia/sdk/repository/DocumentRepository.kt b/writeopia/src/commonMain/kotlin/io/writeopia/sdk/repository/DocumentRepository.kt index c2bcab14b..d007f47d3 100644 --- a/writeopia/src/commonMain/kotlin/io/writeopia/sdk/repository/DocumentRepository.kt +++ b/writeopia/src/commonMain/kotlin/io/writeopia/sdk/repository/DocumentRepository.kt @@ -14,6 +14,7 @@ import kotlinx.datetime.Instant * those need to be passed as parameters. */ interface DocumentRepository : DocumentUpdate, DocumentSearch { + // Change userId for workspace ID in this class. suspend fun loadDocumentsForFolder(folderId: String): List diff --git a/writeopia_models/build.gradle.kts b/writeopia_models/build.gradle.kts index ee4011ac3..b0c969957 100644 --- a/writeopia_models/build.gradle.kts +++ b/writeopia_models/build.gradle.kts @@ -52,6 +52,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} js(IR) { diff --git a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/StoryTypes.kt b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/StoryTypes.kt index 4792da1c4..c00b7bc45 100644 --- a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/StoryTypes.kt +++ b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/StoryTypes.kt @@ -54,6 +54,7 @@ enum class StoryTypes(val type: StoryType) { LOADING(StoryType("loading", 19)), DOCUMENT_LINK(StoryType("document_link", 20)), DIVIDER(StoryType("divider", 21)), + EQUATION(StoryType("equation", 22)), ; companion object { diff --git a/writeopia_ui/build.gradle.kts b/writeopia_ui/build.gradle.kts index 52f93c385..17f490ec0 100644 --- a/writeopia_ui/build.gradle.kts +++ b/writeopia_ui/build.gradle.kts @@ -54,6 +54,8 @@ mavenPublishing { } kotlin { + jvmToolchain(21) + jvm {} androidTarget() diff --git a/writeopia_ui/src/androidMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersAndroid.kt b/writeopia_ui/src/androidMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersAndroid.kt index a416cd168..daa60569b 100644 --- a/writeopia_ui/src/androidMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersAndroid.kt +++ b/writeopia_ui/src/androidMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersAndroid.kt @@ -35,6 +35,7 @@ object DefaultDrawersAndroid : DrawersFactory { generateSection: (Int) -> Unit, receiveExternalFile: (List, Int) -> Unit, onDocumentLinkClick: (String) -> Unit, + equationToImageUrl: String? ): Map { val commonDrawers = CommonDrawers.create( manager, @@ -51,6 +52,7 @@ object DefaultDrawersAndroid : DrawersFactory { isEmptyErase = DefaultDrawersAndroid::emptyErase ), fontFamily = fontFamily, + equationToImageUrl = equationToImageUrl, headerEndContent = { storyStep, drawInfo, _ -> val isTitle = storyStep.tags.any { it.tag.isTitle() } val isCollapsed by lazy { storyStep.tags.any { it.tag == Tag.COLLAPSED } } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt new file mode 100644 index 000000000..3be566c24 --- /dev/null +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt @@ -0,0 +1,187 @@ +package io.writeopia.ui.drawer.content + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import io.writeopia.sdk.model.action.Action +import io.writeopia.sdk.model.draganddrop.DropInfo +import io.writeopia.sdk.models.files.ExternalFile +import io.writeopia.sdk.models.story.StoryStep +import io.writeopia.ui.components.SwipeBox +import io.writeopia.ui.components.multiselection.SelectableByDrag +import io.writeopia.ui.draganddrop.target.DragRowTarget +import io.writeopia.ui.draganddrop.target.DropTargetVerticalDivision +import io.writeopia.ui.draganddrop.target.InBounds +import io.writeopia.ui.draganddrop.target.external.externalImageDropTarget +import io.writeopia.ui.draganddrop.target.external.shouldAcceptImageDrop +import io.writeopia.ui.drawer.StoryStepDrawer +import io.writeopia.ui.icons.WrSdkIcons +import io.writeopia.ui.model.DrawConfig +import io.writeopia.ui.model.DrawInfo + +class EquationDrawer( + private val equationToImageUrl: String, + private val customBackgroundColor: Color, + private val config: DrawConfig, + private val dragIconWidth: Dp, + private val enabled: Boolean, + private val onDragHover: (Int) -> Unit, + private val onDragStart: () -> Unit, + private val onDragStop: () -> Unit, + private val moveRequest: (Action.Move) -> Unit, + private val onSelected: (Boolean, Int) -> Unit, + private val receiveExternalFile: (List, Int) -> Unit, + private val onEditClick: (Int) -> Unit +) : StoryStepDrawer { + + @Composable + override fun Step( + step: StoryStep, + drawInfo: DrawInfo + ) { + val dropInfo = DropInfo(step, drawInfo.position) + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + + SelectableByDrag( + modifier = Modifier.dragAndDropTarget( + shouldStartDragAndDrop = ::shouldAcceptImageDrop, + target = externalImageDropTarget( + onStart = onDragStart, + onEnd = onDragStop, + onEnter = { + onDragHover(drawInfo.position) + }, + onExit = {}, + onFileReceived = { files -> + receiveExternalFile(files, drawInfo.position + 1) + } + ) + ) + ) { isInsideDrag -> + if (isInsideDrag != null) { + LaunchedEffect(isInsideDrag) { + onSelected(isInsideDrag, drawInfo.position) + } + } + + DropTargetVerticalDivision( + modifier = Modifier + ) { inBound, data -> + when (inBound) { + InBounds.OUTSIDE -> {} + InBounds.INSIDE_UP -> { + val position = drawInfo.position - 1 + handleDrag(position, data) + } + + InBounds.INSIDE_DOWN -> { + val position = drawInfo.position + handleDrag(position, data) + } + } + + SwipeBox( + modifier = Modifier.hoverable(interactionSource), + defaultColor = customBackgroundColor, + activeColor = config.selectedColor(), + activeBorderColor = config.selectedBorderColor(), + isOnEditState = drawInfo.selectMode, + swipeListener = { isSelected -> + onSelected(isSelected, drawInfo.position) + }, + ) { + DragRowTarget( + modifier = Modifier.fillMaxWidth(), + dataToDrop = dropInfo, + showIcon = isHovered && enabled, + position = drawInfo.position, + dragIconWidth = dragIconWidth, + onDragStart = onDragStart, + onDragStop = onDragStop, + isHoldDraggable = drawInfo.selectMode, + emptySpaceClick = {}, + onClick = { + onSelected(!drawInfo.selectMode, drawInfo.position) + } + ) { + Box(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { + Row( + modifier = Modifier.align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + modifier = Modifier.background(Color.White) + .padding(6.dp), + model = "${equationToImageUrl}${step.text}", + contentDescription = null, + onError = { println("Error: ${it.result.throwable.message}") } + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Crossfade( + targetState = isHovered, + label = "iconCrossFade", + animationSpec = tween(durationMillis = 200) + ) { show -> + if (show) { + Icon( + WrSdkIcons.edit, + contentDescription = "Pencil", + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(32.dp) + .clip(CircleShape) + .clickable { onEditClick(drawInfo.position) } + .padding(8.dp) + ) + } + } + } + } + } + } + } + } + } + + private fun handleDrag(position: Int, data: DropInfo?) { + onDragHover(position) + + if (data != null) { + moveRequest( + Action.Move( + data.info as StoryStep, + positionFrom = data.positionFrom, + positionTo = position + ) + ) + } + } +} diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt index 3fca22d95..097ed18ac 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt @@ -25,6 +25,7 @@ import io.writeopia.ui.drawer.content.AddButtonDrawer import io.writeopia.ui.drawer.content.AiAnswerDrawer import io.writeopia.ui.drawer.content.DividerDrawer import io.writeopia.ui.drawer.content.DocumentLinkDrawer +import io.writeopia.ui.drawer.content.EquationDrawer import io.writeopia.ui.drawer.content.ImageDrawer import io.writeopia.ui.drawer.content.LastEmptySpace import io.writeopia.ui.drawer.content.LoadingDrawer @@ -64,6 +65,7 @@ object CommonDrawers { receiveExternalFile: (List, Int) -> Unit = { _, _ -> }, headerEndContent: @Composable ((StoryStep, DrawInfo, Boolean) -> Unit)? = null, onDocumentLinkClick: (String) -> Unit, + equationToImageUrl: String? = null ): Map { val commonTextModifier = Modifier.padding( start = drawConfig.codeBlockStartPadding.dp, @@ -245,6 +247,21 @@ object CommonDrawers { val loadingDrawer = LoadingDrawer() + val equationsDrawer = EquationDrawer( + equationToImageUrl = equationToImageUrl ?: "", + customBackgroundColor = MaterialTheme.colorScheme.surfaceVariant, + dragIconWidth = dragIconWidth, + config = drawConfig, + enabled = true, + receiveExternalFile = receiveExternalFile, + onSelected = manager::onSelected, + onDragHover = manager::onDragHover, + onDragStart = manager::onDragStart, + onDragStop = manager::onDragStop, + moveRequest = manager::moveRequest, + onEditClick = manager::onEquationEdition + ) + return buildMap { put(StoryTypes.TEXT.type.number, swipeTextDrawer) put(StoryTypes.ADD_BUTTON.type.number, AddButtonDrawer()) @@ -281,6 +298,7 @@ object CommonDrawers { put(StoryTypes.LOADING.type.number, loadingDrawer) put(StoryTypes.DOCUMENT_LINK.type.number, documentLinkDrawer) put(StoryTypes.DIVIDER.type.number, dividerDrawer) + put(StoryTypes.EQUATION.type.number, equationsDrawer) } } } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/DrawersFactory.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/DrawersFactory.kt index 89a898309..65817161f 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/DrawersFactory.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/DrawersFactory.kt @@ -21,5 +21,6 @@ interface DrawersFactory { generateSection: (Int) -> Unit = {}, receiveExternalFile: (List, Int) -> Unit = { _, _ -> }, onDocumentLinkClick: (String) -> Unit = {}, + equationToImageUrl: String? = null ): Map } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/WrSdkIcons.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/WrSdkIcons.kt index d86ccdb27..25dc687be 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/WrSdkIcons.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/WrSdkIcons.kt @@ -11,6 +11,7 @@ import io.writeopia.ui.icons.all.Files import io.writeopia.ui.icons.all.Link import io.writeopia.ui.icons.all.SquareCheck import io.writeopia.ui.icons.all.WandSparkles +import io.writeopia.ui.icons.all.pencil object WrSdkIcons { @@ -33,4 +34,6 @@ object WrSdkIcons { val ai = WandSparkles val linkPage: ImageVector = Link + + val edit: ImageVector = pencil } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/all/Pencil.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/all/Pencil.kt new file mode 100644 index 000000000..a96b717c9 --- /dev/null +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/icons/all/Pencil.kt @@ -0,0 +1,55 @@ +/* +* Converted using https://composables.com/svg-to-compose +*/ +package io.writeopia.ui.icons.all + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +internal val pencil: ImageVector + get() { + if (_pencil != null) return _pencil!! + + _pencil = ImageVector.Builder( + name = "pencil", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(21.174f, 6.812f) + arcToRelative(1f, 1f, 0f, false, false, -3.986f, -3.987f) + lineTo(3.842f, 16.174f) + arcToRelative(2f, 2f, 0f, false, false, -0.5f, 0.83f) + lineToRelative(-1.321f, 4.352f) + arcToRelative(0.5f, 0.5f, 0f, false, false, 0.623f, 0.622f) + lineToRelative(4.353f, -1.32f) + arcToRelative(2f, 2f, 0f, false, false, 0.83f, -0.497f) + close() + } + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveToRelative(15f, 5f) + lineToRelative(4f, 4f) + } + }.build() + + return _pencil!! + } + +private var _pencil: ImageVector? = null diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt index d0dd4754f..bbfdad136 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt @@ -17,4 +17,5 @@ enum class KeyboardEvent { UNDO, REDO, ACCEPT_AI, + EQUATION, } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt index 4245a6c29..4780f4ce9 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt @@ -139,6 +139,25 @@ class WriteopiaStateManager( acceptSuggestions() } + KeyboardEvent.EQUATION -> { + // Add method that changes based on type and changes either the current + // position or the selected ones + getCurrentStory()?.let { story -> + val position = currentPosition() + + if (position != null) { + changeStoryState( + Action.StoryStateChange( + storyStep = story.copy( + type = StoryTypes.EQUATION.type + ), + position + ) + ) + } + } + } + else -> {} } } @@ -346,6 +365,17 @@ class WriteopiaStateManager( clearSelection() } + fun onEquationEdition(position: Int) { + getStory(position)?.let { storyStep -> + changeStoryState( + stateChange = Action.StoryStateChange( + storyStep.copy(type = StoryTypes.TEXT.type), + position + ) + ) + } + } + /** * At the moment it is only possible to check items not inside groups. Todo: Fix it! * @@ -1221,7 +1251,7 @@ class WriteopiaStateManager( if (lastStateChange == stateChange) return lastStateChange = stateChange - writeopiaManager.changeStoryState(stateChange, _currentStory.value)?.let { state -> + writeopiaManager.changeStoryState(stateChange, _currentStory.value).let { state -> if (trackIt) { backStackManager.addState(_currentStory.value) } diff --git a/writeopia_ui/src/jsMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersJs.kt b/writeopia_ui/src/jsMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersJs.kt index 31e311b0e..e940c14cb 100644 --- a/writeopia_ui/src/jsMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersJs.kt +++ b/writeopia_ui/src/jsMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersJs.kt @@ -41,6 +41,7 @@ object DefaultDrawersJs : DrawersFactory { generateSection: (Int) -> Unit, receiveExternalFile: (List, Int) -> Unit, onDocumentLinkClick: (String) -> Unit, + equationToImageUrl: String? ): Map = CommonDrawers.create( manager, diff --git a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt index 4d2c69c92..291f196d4 100644 --- a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt +++ b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt @@ -44,6 +44,7 @@ object DefaultDrawersDesktop : DrawersFactory { generateSection: (Int) -> Unit, receiveExternalFile: (List, Int) -> Unit, onDocumentLinkClick: (String) -> Unit, + equationToImageUrl: String? ): Map = CommonDrawers.create( manager, @@ -62,6 +63,7 @@ object DefaultDrawersDesktop : DrawersFactory { fontFamily = fontFamily, onDocumentLinkClick = onDocumentLinkClick, receiveExternalFile = receiveExternalFile, + equationToImageUrl = equationToImageUrl, headerEndContent = { storyStep, drawInfo, isHovered -> val isTitle = storyStep.tags.any { it.tag.isTitle() } val isCollapsed by lazy { storyStep.tags.any { it.tag == Tag.COLLAPSED } } diff --git a/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt b/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt index 45d97fb37..e75e735b9 100644 --- a/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt +++ b/writeopia_ui/src/nativeMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersNative.kt @@ -8,12 +8,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import io.writeopia.sdk.models.files.ExternalFile -import io.writeopia.sdk.models.story.StoryTypes import io.writeopia.sdk.models.story.Tag import io.writeopia.ui.drawer.StoryStepDrawer import io.writeopia.ui.icons.WrSdkIcons @@ -34,6 +31,7 @@ object DefaultDrawersNative : DrawersFactory { generateSection: (Int) -> Unit, receiveExternalFile: (List, Int) -> Unit, onDocumentLinkClick: (String) -> Unit, + equationToImageUrl: String? ): Map { val commonDrawers = CommonDrawers.create( manager, @@ -50,6 +48,7 @@ object DefaultDrawersNative : DrawersFactory { isEmptyErase = { _, _ -> false } ), fontFamily = fontFamily, + equationToImageUrl = equationToImageUrl, headerEndContent = { storyStep, drawInfo, _ -> val isTitle = storyStep.tags.any { it.tag.isTitle() } val isCollapsed by lazy { storyStep.tags.any { it.tag == Tag.COLLAPSED } } From dfc2d005999bb2c5977d0fdffca8e2d82af6ae9d Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Tue, 29 Jul 2025 14:57:02 +0200 Subject: [PATCH 14/39] Always showing edit icon for equations when on mobile (#493) Co-authored-by: Leandro Ferreira --- build.gradle.kts | 2 +- .../kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt | 5 +++-- .../kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5fc416ba6..6b1c977eb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,4 +21,4 @@ dagCommand { printModulesInfo = true } -extra["sdkVersion"] = "0.12.0" +extra["sdkVersion"] = "0.12.1" diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt index 3be566c24..d4645cedd 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/EquationDrawer.kt @@ -46,6 +46,7 @@ import io.writeopia.ui.model.DrawConfig import io.writeopia.ui.model.DrawInfo class EquationDrawer( + private val showEditAlways: Boolean, private val equationToImageUrl: String, private val customBackgroundColor: Color, private val config: DrawConfig, @@ -57,7 +58,7 @@ class EquationDrawer( private val moveRequest: (Action.Move) -> Unit, private val onSelected: (Boolean, Int) -> Unit, private val receiveExternalFile: (List, Int) -> Unit, - private val onEditClick: (Int) -> Unit + private val onEditClick: (Int) -> Unit, ) : StoryStepDrawer { @Composable @@ -147,7 +148,7 @@ class EquationDrawer( Spacer(modifier = Modifier.width(6.dp)) Crossfade( - targetState = isHovered, + targetState = isHovered || showEditAlways, label = "iconCrossFade", animationSpec = tween(durationMillis = 200) ) { show -> diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt index 097ed18ac..8c80d869b 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt @@ -248,6 +248,7 @@ object CommonDrawers { val loadingDrawer = LoadingDrawer() val equationsDrawer = EquationDrawer( + showEditAlways = !isDesktop, equationToImageUrl = equationToImageUrl ?: "", customBackgroundColor = MaterialTheme.colorScheme.surfaceVariant, dragIconWidth = dragIconWidth, From 7741d1527fe0978446873341e02474a9bc93923d Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Mon, 4 Aug 2025 14:11:39 +0200 Subject: [PATCH 15/39] header for mobile (#494) Co-authored-by: Leandro Ferreira --- gradle/libs.versions.toml | 2 +- .../ui/drawer/content/HeaderDrawer.kt | 22 +++++++++++-------- .../ui/drawer/factory/CommonDrawers.kt | 1 + 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fd5efdc6c..618eab1a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ ktx = "1.16.0" material = "1.12.0" platformtools = "0.4.0" room = "2.7.2" -agp = "8.11.1" +agp = "8.12.0" kotlin = "2.2.0" composeTest = "1.8.3" webWorkerDriver = "2.1.0" diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt index a7bc0b111..b6e38a81b 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt @@ -43,6 +43,7 @@ import io.writeopia.sdk.models.story.StoryTypes import io.writeopia.ui.drawer.SimpleTextDrawer import io.writeopia.ui.drawer.StoryStepDrawer import io.writeopia.ui.drawer.factory.EndOfText +import io.writeopia.ui.icons.WrSdkIcons import io.writeopia.ui.manager.WriteopiaStateManager import io.writeopia.ui.model.DrawConfig import io.writeopia.ui.model.DrawInfo @@ -60,6 +61,7 @@ import writeopia.writeopia_ui.generated.resources.title */ class HeaderDrawer( private val modifier: Modifier = Modifier, + private val forceEditorButtonAppear: Boolean = false, private val headerClick: () -> Unit = {}, private val textDrawer: () -> SimpleTextDrawer, // private val multipleSelection: (Int) -> Unit, @@ -145,14 +147,14 @@ class HeaderDrawer( } AnimatedVisibility( - isHovered, + isHovered || forceEditorButtonAppear, enter = fadeIn(), exit = fadeOut(), modifier = Modifier .padding(6.dp) .align(Alignment.TopEnd), ) { - Text( + Icon( modifier = Modifier .padding(6.dp) .clip(MaterialTheme.shapes.medium) @@ -161,11 +163,11 @@ class HeaderDrawer( MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.shapes.medium ) - .padding(6.dp), - text = "Edit header", - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.Bold + .padding(8.dp) + .size(16.dp), + imageVector = WrSdkIcons.edit, + contentDescription = "Edit header", + tint = MaterialTheme.colorScheme.onBackground ) } } @@ -182,7 +184,8 @@ fun headerDrawer( drawConfig: DrawConfig, enabled: Boolean = true, isDarkTheme: Boolean, - fontFamily: FontFamily? = null + fontFamily: FontFamily? = null, + forceEditorButtonAppear: Boolean = false, ): StoryStepDrawer = HeaderDrawer( modifier = modifier, @@ -198,10 +201,11 @@ fun headerDrawer( textStyle = { drawConfig.titleStyle(fontFamily) }, selectionState = selectionState, onSelectionLister = {}, - aiExplanation = "" + aiExplanation = "", ) }, headerClick = headerClick, + forceEditorButtonAppear = forceEditorButtonAppear, placeHolderStyle = { drawConfig.titlePlaceHolderStyle(fontFamily) } ) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt index 8c80d869b..593a960f6 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt @@ -223,6 +223,7 @@ object CommonDrawers { drawConfig = drawConfig, fontFamily = fontFamily, isDarkTheme = isDarkTheme, + forceEditorButtonAppear = !isDesktop ) val imageDrawer = ImageDrawer( From 8f009967c38631c6984a027edf76ae7d7c2a749f Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Sun, 10 Aug 2025 03:41:01 +0200 Subject: [PATCH 16/39] Command handler improve (#495) * Using trie * Adding tests * code refactor * removing unused imports --------- Co-authored-by: Leandro Ferreira --- .../import/markdown/CharactersReverseIndex.kt | 47 ----------- .../writeopia/sdk/models/command/Command.kt | 23 +++--- .../ui/edition/TextCommandHandler.kt | 81 +++++++++++++------ .../ui/edition/TextCommandHandlerText.kt | 31 +++++++ 4 files changed, 98 insertions(+), 84 deletions(-) delete mode 100644 plugins/writeopia_import_document/src/commonMain/kotlin/io/writeopia/sdk/import/markdown/CharactersReverseIndex.kt create mode 100644 writeopia_ui/src/commonTest/kotlin/io/writeopia/ui/edition/TextCommandHandlerText.kt diff --git a/plugins/writeopia_import_document/src/commonMain/kotlin/io/writeopia/sdk/import/markdown/CharactersReverseIndex.kt b/plugins/writeopia_import_document/src/commonMain/kotlin/io/writeopia/sdk/import/markdown/CharactersReverseIndex.kt deleted file mode 100644 index a05c015f3..000000000 --- a/plugins/writeopia_import_document/src/commonMain/kotlin/io/writeopia/sdk/import/markdown/CharactersReverseIndex.kt +++ /dev/null @@ -1,47 +0,0 @@ -package io.writeopia.sdk.import.markdown - -import io.writeopia.sdk.models.command.Command -import io.writeopia.sdk.models.command.CommandFactory - -class CharactersReverseIndexParser( - commandList: List = CommandFactory.defaultCommands().toList() -) { - - private val reversedIndex: Map> = createReverseIndexOfCommand(commandList) -} - -fun createReverseIndexOfCommand(commandList: List): Map> { - return commandList.map { command -> commandToIndexes(command) } - .reduce { acc, map -> - buildMap { - putAll(acc) - - map.map { (char, commandIndexList) -> - val indexes = get(char) ?: emptyList() - put(char, indexes + commandIndexList) - } - } - } -} - -private fun commandToIndexes(command: Command): Map> { - val resultMap = mutableMapOf>() - - command.commandText.forEachIndexed { index, char -> - val indexList = resultMap[char] ?: emptyList() - val commandIndex = CommandIndex(commandId = command.commandId, position = index) - - resultMap[char] = indexList + commandIndex - } - - return resultMap -} - -/** - * The command index is the index that each character should be mapped to. Each character can be in - * many commands in a single position. So each character should have a list of [CommandIndex] - */ -data class CommandIndex( - val commandId: Int, - val position: Int -) diff --git a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/command/Command.kt b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/command/Command.kt index 044059a77..102cce6f0 100644 --- a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/command/Command.kt +++ b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/command/Command.kt @@ -1,32 +1,31 @@ package io.writeopia.sdk.models.command -data class Command(val commandId: Int, val commandText: String, val whereToFind: WhereToFind) +data class Command(val commandText: String, val whereToFind: WhereToFind) enum class WhereToFind { START, - ANYWHERE } object CommandFactory { - fun checkItem() = Command(1, "-[] ", WhereToFind.START) + fun checkItem() = Command("-[]", WhereToFind.START) - fun checkItem2() = Command(8, "[] ", WhereToFind.START) + fun checkItem2() = Command("[]", WhereToFind.START) - fun box() = Command(9, "/box ", WhereToFind.START) + fun box() = Command("/box", WhereToFind.START) - fun unOrderedList() = Command(2, "- ", WhereToFind.START) + fun unOrderedList() = Command("-", WhereToFind.START) - fun h1() = Command(3, "# ", WhereToFind.START) + fun h1() = Command("#", WhereToFind.START) - fun h2() = Command(4, "## ", WhereToFind.START) + fun h2() = Command("##", WhereToFind.START) - fun h3() = Command(5, "### ", WhereToFind.START) + fun h3() = Command("###", WhereToFind.START) - fun h4() = Command(6, "#### ", WhereToFind.START) + fun h4() = Command("####", WhereToFind.START) - fun codeBlock() = Command(7, "``` ", WhereToFind.START) + fun codeBlock() = Command("```", WhereToFind.START) - fun divider() = Command(7, "---", WhereToFind.START) + fun divider() = Command("---", WhereToFind.START) fun defaultCommands(): Set = setOf(checkItem(), unOrderedList(), h1(), h2(), h3(), h4(), codeBlock()) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt index c5291419a..c40bd980e 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt @@ -1,11 +1,9 @@ package io.writeopia.ui.edition -import io.writeopia.sdk.models.command.Command import io.writeopia.sdk.models.command.CommandFactory import io.writeopia.sdk.models.command.CommandInfo import io.writeopia.sdk.models.command.CommandTrigger import io.writeopia.sdk.models.command.TypeInfo -import io.writeopia.sdk.models.command.WhereToFind import io.writeopia.sdk.models.story.Decoration import io.writeopia.sdk.models.story.StoryStep import io.writeopia.sdk.models.story.StoryTypes @@ -14,25 +12,30 @@ import io.writeopia.sdk.models.story.TagInfo import io.writeopia.ui.manager.WriteopiaStateManager class TextCommandHandler( - private val commandsMap: Map Unit>, - private val excludeTypes: Set = setOf(StoryTypes.TITLE.type.number) + private val commandsMap: Map Unit>, + private val excludeTypes: Set = setOf(StoryTypes.TITLE.type.number), + private val trie: Trie = Trie() ) { + init { + commandsMap.keys.forEach { trie.insert(it) } + } + fun handleCommand(text: String, step: StoryStep, position: Int): Boolean { - if (excludeTypes.contains(step.type.number)) return false + if (excludeTypes.contains(step.type.number) || text.lastOrNull() != ' ') return false - // Todo(Leandro): Using a reverse index would improve the speed a lot. - val command: Command = commandsMap.keys - .firstOrNull { command -> - when (command.whereToFind) { - WhereToFind.START -> text.startsWith(command.commandText) - WhereToFind.ANYWHERE -> text.contains(command.commandText) - } - } ?: return false + val textArray = text.split(" ") + if (textArray.isEmpty()) return false - commandsMap[command]!!.invoke(step.copy(text = text), position) + val command = textArray[0] + val hasCommand = trie.search(command) - return true + return if (hasCommand) { + commandsMap[command]!!.invoke(step.copy(text = text), position) + true + } else { + false + } } companion object { @@ -41,7 +44,7 @@ class TextCommandHandler( fun defaultCommands(manager: WriteopiaStateManager): TextCommandHandler { return TextCommandHandler( mapOf( - CommandFactory.checkItem() to { _, position -> + CommandFactory.checkItem().commandText to { _, position -> manager.changeStoryType( position, TypeInfo(StoryTypes.CHECK_ITEM.type), @@ -51,7 +54,7 @@ class TextCommandHandler( ) ) }, - CommandFactory.checkItem2() to { _, position -> + CommandFactory.checkItem2().commandText to { _, position -> manager.changeStoryType( position, TypeInfo(StoryTypes.CHECK_ITEM.type), @@ -61,14 +64,14 @@ class TextCommandHandler( ) ) }, - CommandFactory.box() to { _, position -> + CommandFactory.box().commandText to { _, position -> manager.toggleTagForPosition( position, TagInfo(Tag.HIGH_LIGHT_BLOCK), CommandInfo(CommandFactory.box(), CommandTrigger.WRITTEN) ) }, - CommandFactory.unOrderedList() to { _, position -> + CommandFactory.unOrderedList().commandText to { _, position -> manager.changeStoryType( position, TypeInfo(StoryTypes.UNORDERED_LIST_ITEM.type), @@ -78,7 +81,7 @@ class TextCommandHandler( ) ) }, - CommandFactory.h1() to { _, position -> + CommandFactory.h1().commandText to { _, position -> manager.changeStoryType( position, TypeInfo( @@ -92,7 +95,7 @@ class TextCommandHandler( ) ) }, - CommandFactory.h2() to { _, position -> + CommandFactory.h2().commandText to { _, position -> manager.changeStoryType( position, TypeInfo( @@ -106,7 +109,7 @@ class TextCommandHandler( ) ) }, - CommandFactory.h3() to { _, position -> + CommandFactory.h3().commandText to { _, position -> manager.changeStoryType( position, TypeInfo( @@ -120,7 +123,7 @@ class TextCommandHandler( ) ) }, - CommandFactory.h4() to { _, position -> + CommandFactory.h4().commandText to { _, position -> manager.changeStoryType( position, TypeInfo( @@ -134,7 +137,7 @@ class TextCommandHandler( ) ) }, - CommandFactory.codeBlock() to { _, position -> + CommandFactory.codeBlock().commandText to { _, position -> manager.changeStoryType( position, TypeInfo( @@ -147,7 +150,7 @@ class TextCommandHandler( ) ) }, - CommandFactory.divider() to { _, position -> + CommandFactory.divider().commandText to { _, position -> manager.changeStoryType( position, TypeInfo( @@ -165,3 +168,31 @@ class TextCommandHandler( } } } + +class Trie { + private class TrieNode { + val children: MutableMap = mutableMapOf() + var isEndOfWord: Boolean = false + } + + private val root = TrieNode() + + fun insert(word: String) { + var node = root + + word.forEach { char -> node = node.children.getOrPut(char, ::TrieNode) } + node.isEndOfWord = true + } + + fun search(word: String): Boolean { + val node = findNode(word) + return node?.isEndOfWord == true + } + + private fun findNode(prefix: String): TrieNode? { + var node = root + + prefix.forEach { char -> node = node.children[char] ?: return null } + return node + } +} diff --git a/writeopia_ui/src/commonTest/kotlin/io/writeopia/ui/edition/TextCommandHandlerText.kt b/writeopia_ui/src/commonTest/kotlin/io/writeopia/ui/edition/TextCommandHandlerText.kt new file mode 100644 index 000000000..6167cea5f --- /dev/null +++ b/writeopia_ui/src/commonTest/kotlin/io/writeopia/ui/edition/TextCommandHandlerText.kt @@ -0,0 +1,31 @@ +package io.writeopia.ui.edition + +import io.writeopia.sdk.models.story.StoryStep +import io.writeopia.sdk.models.story.StoryTypes +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TextCommandHandlerText { + + @Test + fun `it should be possible to trigger a command`() { + val handler = TextCommandHandler( + commandsMap = mapOf( + "##" to { _, _ -> }, + "command" to { _, _ -> }, + "/box" to { _, _ -> }, + ) + ) + + assertTrue { + handler.handleCommand("## ", StoryStep(type = StoryTypes.TEXT.type), 0) + } + assertTrue { + handler.handleCommand("command ", StoryStep(type = StoryTypes.TEXT.type), 0) + } + assertFalse { + handler.handleCommand("notACommand ", StoryStep(type = StoryTypes.TEXT.type), 0) + } + } +} From 4cffa0c764c704653360a537209f94dce01f1ab6 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Fri, 22 Aug 2025 18:23:28 +0200 Subject: [PATCH 17/39] In document full text search (#496) * Showing search box * Adding FTS in SQLDlight * Injecting InDocumentSearchRepository * Adding insert for Fts * First sketch working * Moving focus to search * Method rename * search in document partially working * Moving focus correctly * ktlint * moving tests --------- Co-authored-by: Leandro Ferreira --- .../io/writeopia/desktop/MainDesktop.kt | 5 + .../di/InDocumentSearchInjection.android.kt | 15 +++ .../folders/di/InDocumentSearchInjection.kt | 12 +++ .../repository/InDocumentSearchRepository.kt | 15 +++ .../di/InDocumentSearchInjection.js.kt | 15 +++ .../di/InDocumentSearchInjection.jvm.kt | 29 ++++++ .../di/InDocumentSearchInjection.native.kt | 15 +++ .../persistence_sqldelight/build.gradle.kts | 2 +- .../dao/StoryStepFtsSqlDelightDao.kt | 29 ++++++ .../writeopia/app/sql/StoryStepEntityFts.sq | 15 +++ .../common/utils/keyboard/KeyboardCommands.kt | 8 +- application/features/editor/build.gradle.kts | 2 + .../writeopia/editor/di/EditorKmpInjector.kt | 8 +- .../ui/desktop/DesktopNoteEditorScreen.kt | 62 ++++++++++++ .../viewmodel/NoteEditorKmpViewModel.kt | 95 +++++++++++++++++-- .../editor/viewmodel/NoteEditorViewModel.kt | 10 ++ .../editor/features/search/FindInText.kt | 32 +++++++ .../writeopia/editor/search/FindInTextTest.kt | 81 ++++++++++++++++ gradle/libs.versions.toml | 4 +- .../core/tracker/OnUpdateDocumentTracker.kt | 10 +- .../build.gradle.kts | 2 +- .../sqldelight/dao/DocumentSqlDao.kt | 1 + .../io/writeopia/ui/keyboard/KeyboardEvent.kt | 1 + 23 files changed, 453 insertions(+), 15 deletions(-) create mode 100644 application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt create mode 100644 application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.kt create mode 100644 application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt create mode 100644 application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt create mode 100644 application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt create mode 100644 application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt create mode 100644 application/core/persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sqldelight/dao/StoryStepFtsSqlDelightDao.kt create mode 100644 application/core/persistence_sqldelight/src/commonMain/sqldelight/io/writeopia/app/sql/StoryStepEntityFts.sq create mode 100644 application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/search/FindInText.kt create mode 100644 application/features/editor/src/jvmTest/kotlin/io/writeopia/editor/search/FindInTextTest.kt diff --git a/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt b/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt index 7d231acb6..e42b7bb6b 100644 --- a/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt +++ b/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt @@ -174,6 +174,11 @@ private fun ApplicationScope.App(onCloseRequest: () -> Unit = ::exitApplication) false } + KeyboardCommands.isSearchEvent(keyEvent) -> { + sendEvent(KeyboardEvent.SEARCH) + false + } + else -> false } } diff --git a/application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt b/application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt new file mode 100644 index 000000000..aec501d8f --- /dev/null +++ b/application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt @@ -0,0 +1,15 @@ +package io.writeopia.core.folders.di + +import io.writeopia.core.folders.repository.InDocumentSearchRepository + +actual class InDocumentSearchInjection { + actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository { + TODO("Not yet implemented") + } + + actual companion object { + actual fun singleton(): InDocumentSearchInjection { + TODO("Not yet implemented") + } + } +} diff --git a/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.kt b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.kt new file mode 100644 index 000000000..df573ce26 --- /dev/null +++ b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.kt @@ -0,0 +1,12 @@ +package io.writeopia.core.folders.di + +import io.writeopia.core.folders.repository.InDocumentSearchRepository + +expect class InDocumentSearchInjection { + + fun provideInDocumentSearchRepo(): InDocumentSearchRepository + + companion object { + fun singleton(): InDocumentSearchInjection + } +} diff --git a/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt new file mode 100644 index 000000000..ccb6f4aa5 --- /dev/null +++ b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt @@ -0,0 +1,15 @@ +package io.writeopia.core.folders.repository + +import io.writeopia.sdk.models.story.StoryStep +import io.writeopia.sqldelight.dao.StoryStepFtsSqlDelightDao + +class InDocumentSearchRepository(private val storyStepFtsSqlDelightDao: StoryStepFtsSqlDelightDao) { + + fun searchInDocument(query: String, documentId: String): Set { + return storyStepFtsSqlDelightDao.searchInDocument(query, documentId) + } + + suspend fun insertForFts(storyStep: StoryStep, documentId: String, position: Int) { + storyStepFtsSqlDelightDao.insertForFts(storyStep, documentId, position) + } +} diff --git a/application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt b/application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt new file mode 100644 index 000000000..aec501d8f --- /dev/null +++ b/application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt @@ -0,0 +1,15 @@ +package io.writeopia.core.folders.di + +import io.writeopia.core.folders.repository.InDocumentSearchRepository + +actual class InDocumentSearchInjection { + actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository { + TODO("Not yet implemented") + } + + actual companion object { + actual fun singleton(): InDocumentSearchInjection { + TODO("Not yet implemented") + } + } +} diff --git a/application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt b/application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt new file mode 100644 index 000000000..2ac7de7b7 --- /dev/null +++ b/application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt @@ -0,0 +1,29 @@ +package io.writeopia.core.folders.di + +import io.writeopia.core.folders.repository.InDocumentSearchRepository +import io.writeopia.sql.WriteopiaDb +import io.writeopia.sqldelight.dao.StoryStepFtsSqlDelightDao +import io.writeopia.sqldelight.di.WriteopiaDbInjector + +actual class InDocumentSearchInjection private constructor( + private val writeopiaDb: WriteopiaDb? +) { + private var inDocumentSearchRepository: InDocumentSearchRepository? = null + + private fun provideFtsSearchDao(): StoryStepFtsSqlDelightDao = + StoryStepFtsSqlDelightDao(writeopiaDb) + + actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository = + inDocumentSearchRepository ?: InDocumentSearchRepository( + provideFtsSearchDao() + ).also { + inDocumentSearchRepository = it + } + + actual companion object { + private var instance: InDocumentSearchInjection? = null + + actual fun singleton(): InDocumentSearchInjection = + instance ?: InDocumentSearchInjection(WriteopiaDbInjector.singleton()?.database) + } +} diff --git a/application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt b/application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt new file mode 100644 index 000000000..aec501d8f --- /dev/null +++ b/application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt @@ -0,0 +1,15 @@ +package io.writeopia.core.folders.di + +import io.writeopia.core.folders.repository.InDocumentSearchRepository + +actual class InDocumentSearchInjection { + actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository { + TODO("Not yet implemented") + } + + actual companion object { + actual fun singleton(): InDocumentSearchInjection { + TODO("Not yet implemented") + } + } +} diff --git a/application/core/persistence_sqldelight/build.gradle.kts b/application/core/persistence_sqldelight/build.gradle.kts index ca4bfec3a..8a88a0df7 100644 --- a/application/core/persistence_sqldelight/build.gradle.kts +++ b/application/core/persistence_sqldelight/build.gradle.kts @@ -72,7 +72,7 @@ sqldelight { databases { create("WriteopiaDb") { packageName.set("io.writeopia.sql") - dialect("app.cash.sqldelight:sqlite-3-30-dialect:2.0.2") + dialect("app.cash.sqldelight:sqlite-3-30-dialect:2.1.0") dependency(project(":plugins:writeopia_persistence_sqldelight")) generateAsync.set(true) } diff --git a/application/core/persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sqldelight/dao/StoryStepFtsSqlDelightDao.kt b/application/core/persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sqldelight/dao/StoryStepFtsSqlDelightDao.kt new file mode 100644 index 000000000..6359852f3 --- /dev/null +++ b/application/core/persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sqldelight/dao/StoryStepFtsSqlDelightDao.kt @@ -0,0 +1,29 @@ +package io.writeopia.sqldelight.dao + +import io.writeopia.app.sql.StoryStepEntityFtsQueries +import io.writeopia.app.sql.TokenEntityQueries +import io.writeopia.sdk.models.story.StoryStep +import io.writeopia.sdk.sql.StoryStepEntityQueries +import io.writeopia.sql.WriteopiaDb + +class StoryStepFtsSqlDelightDao(writeopiaDb: WriteopiaDb?) { + + private val storyStepEntityQueries: StoryStepEntityFtsQueries? = + writeopiaDb?.storyStepEntityFtsQueries + + fun searchInDocument(query: String, documentId: String): Set = + storyStepEntityQueries?.searchFts(query) + ?.executeAsList() + ?.filter { (_, documentIdFts) -> documentIdFts == documentId } + ?.mapNotNullTo(mutableSetOf()) { (position, _) -> position?.toInt() } + ?: emptySet() + + suspend fun insertForFts(storyStep: StoryStep, documentId: String, position: Int) { + storyStepEntityQueries?.insertFts( + text = storyStep.text, + id = storyStep.id, + document_id = documentId, + position = position.toLong() + ) + } +} diff --git a/application/core/persistence_sqldelight/src/commonMain/sqldelight/io/writeopia/app/sql/StoryStepEntityFts.sq b/application/core/persistence_sqldelight/src/commonMain/sqldelight/io/writeopia/app/sql/StoryStepEntityFts.sq new file mode 100644 index 000000000..6bedc2b5d --- /dev/null +++ b/application/core/persistence_sqldelight/src/commonMain/sqldelight/io/writeopia/app/sql/StoryStepEntityFts.sq @@ -0,0 +1,15 @@ +CREATE VIRTUAL TABLE storyStepEntityFts USING fts4( + text TEXT, + id TEXT, + document_id TEXT UNINDEXED, + position INTEGER UNINDEXED +); + +searchFts: +SELECT position, document_id +FROM storyStepEntityFts +WHERE storyStepEntityFts MATCH :query || '*'; + +insertFts: +INSERT INTO storyStepEntityFts(text, id, document_id, position) +VALUES (?, ?, ?, ?); diff --git a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt index 1d585ca3d..44347ee7b 100644 --- a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt +++ b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt @@ -34,6 +34,11 @@ object KeyboardCommands { keyEvent.key.keyCode == Key.B.keyCode && keyEvent.type == KeyEventType.KeyUp + fun isSearchEvent(keyEvent: KeyEvent) = + keyEvent.isCommandTrigger() && + keyEvent.key.keyCode == Key.F.keyCode && + keyEvent.type == KeyEventType.KeyUp + fun isItalicEvent(keyEvent: KeyEvent) = keyEvent.isCommandTrigger() && keyEvent.key.keyCode == Key.I.keyCode && @@ -85,6 +90,7 @@ object KeyboardCommands { keyEvent.type == KeyEventType.KeyUp fun isEquationEvent(keyEvent: KeyEvent) = - keyEvent.key.keyCode == Key.E.keyCode && + keyEvent.isCommandTrigger() && + keyEvent.key.keyCode == Key.E.keyCode && keyEvent.type == KeyEventType.KeyUp } diff --git a/application/features/editor/build.gradle.kts b/application/features/editor/build.gradle.kts index 518444443..9b1d17f58 100644 --- a/application/features/editor/build.gradle.kts +++ b/application/features/editor/build.gradle.kts @@ -63,6 +63,8 @@ kotlin { implementation(compose.foundation) implementation(compose.material3) implementation(libs.compose.navigation) + + implementation(libs.kotlin.test) } } diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/di/EditorKmpInjector.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/di/EditorKmpInjector.kt index 37655b4b8..bf9542fea 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/di/EditorKmpInjector.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/di/EditorKmpInjector.kt @@ -7,6 +7,7 @@ import io.writeopia.auth.core.manager.AuthRepository import io.writeopia.core.configuration.di.AppConfigurationInjector import io.writeopia.core.configuration.di.UiConfigurationCoreInjector import io.writeopia.core.folders.di.FoldersInjector +import io.writeopia.core.folders.di.InDocumentSearchInjection import io.writeopia.di.OllamaInjection import io.writeopia.editor.features.editor.copy.CopyManager import io.writeopia.editor.features.editor.viewmodel.NoteEditorKmpViewModel @@ -34,7 +35,9 @@ class EditorKmpInjector private constructor( private val keyboardEventFlow: Flow, private val appConfigurationInjector: AppConfigurationInjector = AppConfigurationInjector.singleton(), - private val ollamaInjection: OllamaInjection? = null + private val ollamaInjection: OllamaInjection? = null, + private val inDocumentSearchInjection: InDocumentSearchInjection = + InDocumentSearchInjection.singleton(), ) : TextEditorInjector { private fun provideDocumentRepository(): DocumentRepository = @@ -75,7 +78,8 @@ class EditorKmpInjector private constructor( keyboardEventFlow = keyboardEventFlow, copyManager = copyManager, workspaceConfigRepository = appConfigurationInjector.provideWorkspaceConfigRepository(), - authRepository = authCoreInjection.provideAuthRepository() + authRepository = authCoreInjection.provideAuthRepository(), + inDocumentSearchRepository = inDocumentSearchInjection.provideInDocumentSearchRepo() ) @Composable diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt index 99d12aab2..661646dd9 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt @@ -1,20 +1,28 @@ package io.writeopia.editor.features.editor.ui.desktop +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Lock import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -22,11 +30,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.unit.dp +import io.writeopia.common.utils.icons.WrIcons import io.writeopia.commonui.dialogs.confirmation.DeleteConfirmationDialog import io.writeopia.editor.features.editor.ui.desktop.edit.menu.SideEditorOptions import io.writeopia.editor.features.editor.ui.folders.FolderSelectionDialog import io.writeopia.editor.features.editor.viewmodel.NoteEditorViewModel +import io.writeopia.theme.WriteopiaTheme import io.writeopia.ui.drawer.factory.DrawersFactory @Composable @@ -75,6 +89,54 @@ fun DesktopNoteEditorScreen( mutableStateOf(false) } + val textState by noteEditorViewModel.searchText.collectAsState() + val showSearch by noteEditorViewModel.showSearchState.collectAsState() + val shape = MaterialTheme.shapes.medium + + AnimatedVisibility( + showSearch, + enter = fadeIn(animationSpec = tween(durationMillis = 150)), + exit = fadeOut(animationSpec = tween(durationMillis = 150)) + ) { + Box(modifier = Modifier.padding(6.dp)) { + val focusRequester = remember { FocusRequester() } + + BasicTextField( + value = textState, + onValueChange = noteEditorViewModel::searchInDocument, + modifier = Modifier.defaultMinSize(minWidth = 160.dp) + .focusRequester(focusRequester) + .padding(12.dp) + .background(WriteopiaTheme.colorScheme.cardBg, shape) + .border(1.dp, MaterialTheme.colorScheme.outline, shape) + .padding(8.dp), + singleLine = true, + textStyle = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), + ) + + Icon( + imageVector = WrIcons.close, + contentDescription = "Close", + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .padding(4.dp) + .size(20.dp) + .clip(CircleShape) + .clickable(onClick = noteEditorViewModel::hideSearch) + .background(WriteopiaTheme.colorScheme.cardBg, CircleShape) + .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape) + .padding(4.dp) + ) + + LaunchedEffect(key1 = Unit) { + focusRequester.requestFocus() + } + } + } + SideEditorOptions( modifier = Modifier .padding(horizontal = 10.dp, vertical = 40.dp) diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt index cd103c91b..d52fa33ce 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt @@ -12,7 +12,9 @@ import io.writeopia.common.utils.toList import io.writeopia.commonui.dtos.MenuItemUi import io.writeopia.commonui.extensions.toFolderUi import io.writeopia.core.folders.repository.FolderRepository +import io.writeopia.core.folders.repository.InDocumentSearchRepository import io.writeopia.editor.features.editor.copy.CopyManager +import io.writeopia.editor.features.search.FindInText import io.writeopia.editor.model.EditState import io.writeopia.model.Font import io.writeopia.models.interfaces.configuration.WorkspaceConfigRepository @@ -25,7 +27,9 @@ import io.writeopia.sdk.model.action.Action import io.writeopia.sdk.model.story.StoryState import io.writeopia.sdk.models.document.Document import io.writeopia.sdk.models.files.ExternalFile +import io.writeopia.sdk.models.id.GenerateId import io.writeopia.sdk.models.span.Span +import io.writeopia.sdk.models.span.SpanInfo import io.writeopia.sdk.models.story.StoryTypes import io.writeopia.sdk.models.utils.ResultData import io.writeopia.sdk.persistence.core.tracker.OnUpdateDocumentTracker @@ -75,6 +79,7 @@ class NoteEditorKmpViewModel( private val keyboardEventFlow: Flow, private val copyManager: CopyManager, private val authRepository: AuthRepository, + private val inDocumentSearchRepository: InDocumentSearchRepository ) : NoteEditorViewModel, ViewModel(), BackstackInform by writeopiaManager, @@ -105,6 +110,7 @@ class NoteEditorKmpViewModel( KeyboardEvent.CANCEL -> { writeopiaManager.clearSelection() + hideSearch() aiJob?.cancel() } @@ -116,6 +122,10 @@ class NoteEditorKmpViewModel( writeopiaManager.redo() } + KeyboardEvent.SEARCH -> { + showSearch() + } + else -> {} } } @@ -128,6 +138,19 @@ class NoteEditorKmpViewModel( private var isDarkTheme: Boolean = true + private val _showSearch = MutableStateFlow(false) + private val _searchText = MutableStateFlow("") + + override val showSearchState: StateFlow = _showSearch.asStateFlow() + override val searchText: StateFlow = _searchText.asStateFlow() + + private val findsOfSearch: Flow> = + combine(writeopiaManager.documentInfo, searchText) { info, query -> + info.id to query + }.map { (documentId, query) -> + inDocumentSearchRepository.searchInDocument(query, documentId) + } + /** * This property defines if the document should be edited (you can write in it, for example) */ @@ -212,14 +235,43 @@ class NoteEditorKmpViewModel( @OptIn(ExperimentalCoroutinesApi::class) override val toDrawWithDecoration: StateFlow by lazy { - val infoFlow = documentId.flatMapLatest { - documentRepository.listenForDocumentInfoById(it) - } + val infoFlow = documentId.flatMapLatest(documentRepository::listenForDocumentInfoById) + + val toDraw = combine( + writeopiaManager.toDraw, + findsOfSearch, + searchText, + _showSearch + ) { drawState, finds, query, showSearch -> + if (finds.isEmpty() && showSearch) return@combine drawState.copy(focus = null) + if (finds.isEmpty()) return@combine drawState + + val mutableStories = drawState.stories.toMutableList() + + finds.forEach { position -> + val realPosition = minOf(position * 2, mutableStories.lastIndex) + val toDraw = mutableStories[realPosition] + val story = toDraw.storyStep + + val findSpans = FindInText.findInText(story.text ?: "", query) + .map { (start, end) -> + SpanInfo.create(start, end, Span.HIGHLIGHT_YELLOW) + } - writeopiaManager.toDraw.flatMapLatest { drawState -> - infoFlow.map { info -> - drawState to info + mutableStories[realPosition] = + toDraw.copy( + storyStep = story.copy( + spans = story.spans + findSpans, + localId = GenerateId.generate() + ) + ) } + + drawState.copy(stories = mutableStories, focus = null) + } + + toDraw.flatMapLatest { drawState -> + infoFlow.map { info -> drawState to info } }.map { (drawState, info) -> val imageVector = info?.icon ?.label @@ -328,7 +380,20 @@ class NoteEditorKmpViewModel( writeopiaManager.loadDocument(document) writeopiaManager.saveOnStoryChanges( OnUpdateDocumentTracker( - documentRepository + documentRepository, + onStoryStepUpdate = { storyStep, position -> + inDocumentSearchRepository.insertForFts(storyStep, documentId, position) + }, + onDocumentUpdate = { document -> + document.content + .forEach { (position, storyStep) -> + inDocumentSearchRepository.insertForFts( + storyStep, + documentId, + position + ) + } + } ) ) } @@ -609,6 +674,22 @@ class NoteEditorKmpViewModel( } } + override fun showSearch() { + _showSearch.value = true + } + + override fun hideSearch() { + viewModelScope.launch { + _showSearch.value = false + delay(100) + _searchText.value = "" + } + } + + override fun searchInDocument(query: String) { + _searchText.value = query + } + private fun documentPrompt(promptFn: (String, String, String) -> Flow>) { if (ollamaRepository == null) return diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt index f24773dca..292dd2ce6 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt @@ -46,6 +46,16 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { val notFavorite: StateFlow + val showSearchState: StateFlow + + val searchText: StateFlow + + fun showSearch() + + fun hideSearch() + + fun searchInDocument(query: String) + fun toggleEditable() fun deleteSelection() diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/search/FindInText.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/search/FindInText.kt new file mode 100644 index 000000000..aaaca40c6 --- /dev/null +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/search/FindInText.kt @@ -0,0 +1,32 @@ +package io.writeopia.editor.features.search + +object FindInText { + + /** + * Finds the start and end indices of all occurrences of a query string within a given text. + * + * @param text The string to search within. + * @param query The string to search for. + * @return A list of pairs, where each pair represents the start and end indices of a found occurrence. + * The end index is exclusive, meaning it is the index of the character immediately following the match. + */ + fun findInText(text: String, query: String): List> { + val results = mutableListOf>() + if (query.isEmpty()) { + return results + } + + var startIndex = 0 + while (true) { + val foundIndex = text.indexOf(query, startIndex) + if (foundIndex == -1) { + break + } + val endIndex = foundIndex + query.length + results.add(Pair(foundIndex, endIndex)) + startIndex = foundIndex + 1 + } + + return results + } +} diff --git a/application/features/editor/src/jvmTest/kotlin/io/writeopia/editor/search/FindInTextTest.kt b/application/features/editor/src/jvmTest/kotlin/io/writeopia/editor/search/FindInTextTest.kt new file mode 100644 index 000000000..dcc74603a --- /dev/null +++ b/application/features/editor/src/jvmTest/kotlin/io/writeopia/editor/search/FindInTextTest.kt @@ -0,0 +1,81 @@ +package io.writeopia.editor.search + +import io.writeopia.editor.features.search.FindInText +import kotlin.test.Test +import kotlin.test.assertEquals + +class FindInTextTest { + + @Test + fun testSingleOccurrence() { + val text = "hello world" + val query = "world" + val expected = listOf(Pair(6, 11)) + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } + + @Test + fun testMultipleOccurrences() { + val text = "the quick brown fox jumps over the lazy dog" + val query = "the" + val expected = listOf(Pair(0, 3), Pair(31, 34)) + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } + + @Test + fun testOverlappingOccurrences() { + val text = "ababa" + val query = "aba" + + val expected = listOf(Pair(0, 3), Pair(2, 5)) + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } + + @Test + fun testNoOccurrence() { + val text = "this is a test" + val query = "kotlin" + val expected = emptyList>() + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } + + @Test + fun testEmptyQuery() { + val text = "some text" + val query = "" + val expected = emptyList>() + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } + + @Test + fun testEmptyText() { + val text = "" + val query = "query" + val expected = emptyList>() + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } + + @Test + fun testQueryAtStartAndEnd() { + val text = "test_test" + val query = "test" + val expected = listOf(Pair(0, 4), Pair(5, 9)) + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } + + @Test + fun testCaseSensitivity() { + val text = "Hello World" + val query = "hello" // Case mismatch + val expected = emptyList>() + val actual = FindInText.findInText(text, query) + assertEquals(expected, actual) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 618eab1a6..f66c6dafd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ accompanist = "0.36.0" coilVersion = "3.0.4" composeBom = "2025.05.01" composeShimmer = "1.0.5" -espressoCore = "3.6.1" +espressoCore = "3.7.0" firebaseAdmin = "9.2.0" firebaseAuthProvider = "1.5.0" junit = "1.2.1" @@ -17,7 +17,7 @@ ktx = "1.16.0" material = "1.12.0" platformtools = "0.4.0" room = "2.7.2" -agp = "8.12.0" +agp = "8.12.1" kotlin = "2.2.0" composeTest = "1.8.3" webWorkerDriver = "2.1.0" diff --git a/plugins/writeopia_persistence_core/src/commonMain/kotlin/io/writeopia/sdk/persistence/core/tracker/OnUpdateDocumentTracker.kt b/plugins/writeopia_persistence_core/src/commonMain/kotlin/io/writeopia/sdk/persistence/core/tracker/OnUpdateDocumentTracker.kt index 25c2e849c..f8db07a21 100644 --- a/plugins/writeopia_persistence_core/src/commonMain/kotlin/io/writeopia/sdk/persistence/core/tracker/OnUpdateDocumentTracker.kt +++ b/plugins/writeopia_persistence_core/src/commonMain/kotlin/io/writeopia/sdk/persistence/core/tracker/OnUpdateDocumentTracker.kt @@ -9,6 +9,7 @@ import io.writeopia.sdk.model.story.LastEdit import io.writeopia.sdk.model.story.StoryState import io.writeopia.sdk.models.document.Document import io.writeopia.sdk.models.id.GenerateId +import io.writeopia.sdk.models.story.StoryStep import io.writeopia.sdk.models.story.StoryTypes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -16,7 +17,9 @@ import kotlinx.datetime.Clock class OnUpdateDocumentTracker( private val documentUpdate: DocumentUpdate, - private val documentFilter: DocumentFilter = DocumentFilterObject + private val documentFilter: DocumentFilter = DocumentFilterObject, + private val onStoryStepUpdate: suspend (StoryStep, Int) -> Unit = { _, _ -> }, + private val onDocumentUpdate: suspend (Document) -> Unit = {} ) : DocumentTracker { override suspend fun saveOnStoryChanges( @@ -36,6 +39,8 @@ class OnUpdateDocumentTracker( documentId = documentInfo.id, ) + onStoryStepUpdate(lastEdit.storyStep, lastEdit.position) + val stories = storyState.stories val titleFromContent = stories.values .firstOrNull { storyStep -> @@ -85,6 +90,7 @@ class OnUpdateDocumentTracker( ) documentUpdate.saveDocument(document, documentInfo.userId) + onDocumentUpdate(document) } is LastEdit.InfoEdition -> { @@ -116,6 +122,8 @@ class OnUpdateDocumentTracker( position = lastEdit.position, documentId = documentInfo.id ) + + onStoryStepUpdate(lastEdit.storyStep, lastEdit.position) } } diff --git a/plugins/writeopia_persistence_sqldelight/build.gradle.kts b/plugins/writeopia_persistence_sqldelight/build.gradle.kts index 0bf931fd9..f7c11c1ff 100644 --- a/plugins/writeopia_persistence_sqldelight/build.gradle.kts +++ b/plugins/writeopia_persistence_sqldelight/build.gradle.kts @@ -103,7 +103,7 @@ sqldelight { databases { create("WriteopiaDb") { packageName.set("io.writeopia.sdk.sql") - dialect("app.cash.sqldelight:sqlite-3-30-dialect:2.0.2") + dialect("app.cash.sqldelight:sqlite-3-30-dialect:2.1.0") generateAsync.set(true) } } diff --git a/plugins/writeopia_persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sdk/persistence/sqldelight/dao/DocumentSqlDao.kt b/plugins/writeopia_persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sdk/persistence/sqldelight/dao/DocumentSqlDao.kt index 0b2c6e632..9a61f2028 100644 --- a/plugins/writeopia_persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sdk/persistence/sqldelight/dao/DocumentSqlDao.kt +++ b/plugins/writeopia_persistence_sqldelight/src/commonMain/kotlin/io/writeopia/sdk/persistence/sqldelight/dao/DocumentSqlDao.kt @@ -21,6 +21,7 @@ import kotlinx.datetime.Instant class DocumentSqlDao( private val documentQueries: DocumentEntityQueries?, private val storyStepQueries: StoryStepEntityQueries?, + ) : DocumentSearch { override suspend fun search(query: String, userId: String, companyId: String?): List = diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt index bbfdad136..22fd70247 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/keyboard/KeyboardEvent.kt @@ -18,4 +18,5 @@ enum class KeyboardEvent { REDO, ACCEPT_AI, EQUATION, + SEARCH } From 1152973d4e15cf8d88fc054439d819e4f410eb78 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Sun, 24 Aug 2025 23:19:55 +0200 Subject: [PATCH 18/39] Removing escape for navigation (#497) Co-authored-by: Leandro Ferreira --- .../src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt | 6 ++---- .../io/writeopia/common/utils/keyboard/KeyboardCommands.kt | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt b/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt index e42b7bb6b..6895bec19 100644 --- a/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt +++ b/application/composeApp/src/jvmMain/kotlin/io/writeopia/desktop/MainDesktop.kt @@ -152,7 +152,7 @@ private fun ApplicationScope.App(onCloseRequest: () -> Unit = ::exitApplication) KeyboardCommands.isCancelEvent(keyEvent) -> { sendEvent(KeyboardEvent.CANCEL) - false + true } KeyboardCommands.isAcceptAiEvent(keyEvent) -> { @@ -200,9 +200,7 @@ private fun ApplicationScope.App(onCloseRequest: () -> Unit = ::exitApplication) onCloseRequest = onCloseRequest, title = "", state = windowState, - onPreviewKeyEvent = { keyEvent -> - handleKeyboardEvent(keyEvent) - }, + onPreviewKeyEvent = handleKeyboardEvent, icon = CommonImages.logo() ) { // Synchronize the topbar on Windows OS with the System Theme diff --git a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt index 44347ee7b..f5877376f 100644 --- a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt +++ b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/keyboard/KeyboardCommands.kt @@ -82,8 +82,7 @@ object KeyboardCommands { keyEvent.type == KeyEventType.KeyUp fun isCancelEvent(keyEvent: KeyEvent) = - keyEvent.key.keyCode == Key.Escape.keyCode && - keyEvent.type == KeyEventType.KeyUp + keyEvent.key.keyCode == Key.Escape.keyCode fun isAcceptAiEvent(keyEvent: KeyEvent) = keyEvent.key.keyCode == Key.Tab.keyCode && From d6c334e9013b3231347c0855b391cebbad79fb96 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Thu, 28 Aug 2025 21:54:30 +0200 Subject: [PATCH 19/39] Fixing android compilation (#498) * Fixing android compilation * removing import * fixing web --------- Co-authored-by: Leandro Ferreira --- .../di/InDocumentSearchInjection.android.kt | 21 +++++++++++++++---- .../repository/InDocumentSearchRepository.kt | 11 +++------- .../InDocumentSearchSqlRepository.kt | 17 +++++++++++++++ .../di/InDocumentSearchInjection.js.kt | 21 +++++++++++++++---- .../di/InDocumentSearchInjection.jvm.kt | 5 +++-- .../di/InDocumentSearchInjection.native.kt | 4 ++-- .../room/WriteopiaAplicationDatabase.kt | 2 +- 7 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchSqlRepository.kt diff --git a/application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt b/application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt index aec501d8f..edbb2653e 100644 --- a/application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt +++ b/application/core/documents/src/androidMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.android.kt @@ -1,15 +1,28 @@ package io.writeopia.core.folders.di import io.writeopia.core.folders.repository.InDocumentSearchRepository +import io.writeopia.sdk.models.story.StoryStep actual class InDocumentSearchInjection { actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository { - TODO("Not yet implemented") + return object : InDocumentSearchRepository { + override fun searchInDocument( + query: String, + documentId: String + ): Set = emptySet() + + override suspend fun insertForFts( + storyStep: StoryStep, + documentId: String, + position: Int + ) {} + } } actual companion object { - actual fun singleton(): InDocumentSearchInjection { - TODO("Not yet implemented") - } + private var instance: InDocumentSearchInjection? = null + + actual fun singleton(): InDocumentSearchInjection = + instance ?: InDocumentSearchInjection() } } diff --git a/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt index ccb6f4aa5..c2fd468af 100644 --- a/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt +++ b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchRepository.kt @@ -1,15 +1,10 @@ package io.writeopia.core.folders.repository import io.writeopia.sdk.models.story.StoryStep -import io.writeopia.sqldelight.dao.StoryStepFtsSqlDelightDao -class InDocumentSearchRepository(private val storyStepFtsSqlDelightDao: StoryStepFtsSqlDelightDao) { +interface InDocumentSearchRepository { - fun searchInDocument(query: String, documentId: String): Set { - return storyStepFtsSqlDelightDao.searchInDocument(query, documentId) - } + fun searchInDocument(query: String, documentId: String): Set - suspend fun insertForFts(storyStep: StoryStep, documentId: String, position: Int) { - storyStepFtsSqlDelightDao.insertForFts(storyStep, documentId, position) - } + suspend fun insertForFts(storyStep: StoryStep, documentId: String, position: Int) } diff --git a/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchSqlRepository.kt b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchSqlRepository.kt new file mode 100644 index 000000000..799f7f306 --- /dev/null +++ b/application/core/documents/src/commonMain/kotlin/io/writeopia/core/folders/repository/InDocumentSearchSqlRepository.kt @@ -0,0 +1,17 @@ +package io.writeopia.core.folders.repository + +import io.writeopia.sdk.models.story.StoryStep +import io.writeopia.sqldelight.dao.StoryStepFtsSqlDelightDao + +class InDocumentSearchSqlRepository( + private val storyStepFtsSqlDelightDao: StoryStepFtsSqlDelightDao +) : InDocumentSearchRepository { + + override fun searchInDocument(query: String, documentId: String): Set { + return storyStepFtsSqlDelightDao.searchInDocument(query, documentId) + } + + override suspend fun insertForFts(storyStep: StoryStep, documentId: String, position: Int) { + storyStepFtsSqlDelightDao.insertForFts(storyStep, documentId, position) + } +} diff --git a/application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt b/application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt index aec501d8f..edbb2653e 100644 --- a/application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt +++ b/application/core/documents/src/jsMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.js.kt @@ -1,15 +1,28 @@ package io.writeopia.core.folders.di import io.writeopia.core.folders.repository.InDocumentSearchRepository +import io.writeopia.sdk.models.story.StoryStep actual class InDocumentSearchInjection { actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository { - TODO("Not yet implemented") + return object : InDocumentSearchRepository { + override fun searchInDocument( + query: String, + documentId: String + ): Set = emptySet() + + override suspend fun insertForFts( + storyStep: StoryStep, + documentId: String, + position: Int + ) {} + } } actual companion object { - actual fun singleton(): InDocumentSearchInjection { - TODO("Not yet implemented") - } + private var instance: InDocumentSearchInjection? = null + + actual fun singleton(): InDocumentSearchInjection = + instance ?: InDocumentSearchInjection() } } diff --git a/application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt b/application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt index 2ac7de7b7..85d9d250a 100644 --- a/application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt +++ b/application/core/documents/src/jvmMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.jvm.kt @@ -1,6 +1,7 @@ package io.writeopia.core.folders.di import io.writeopia.core.folders.repository.InDocumentSearchRepository +import io.writeopia.core.folders.repository.InDocumentSearchSqlRepository import io.writeopia.sql.WriteopiaDb import io.writeopia.sqldelight.dao.StoryStepFtsSqlDelightDao import io.writeopia.sqldelight.di.WriteopiaDbInjector @@ -8,13 +9,13 @@ import io.writeopia.sqldelight.di.WriteopiaDbInjector actual class InDocumentSearchInjection private constructor( private val writeopiaDb: WriteopiaDb? ) { - private var inDocumentSearchRepository: InDocumentSearchRepository? = null + private var inDocumentSearchRepository: InDocumentSearchSqlRepository? = null private fun provideFtsSearchDao(): StoryStepFtsSqlDelightDao = StoryStepFtsSqlDelightDao(writeopiaDb) actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository = - inDocumentSearchRepository ?: InDocumentSearchRepository( + inDocumentSearchRepository ?: InDocumentSearchSqlRepository( provideFtsSearchDao() ).also { inDocumentSearchRepository = it diff --git a/application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt b/application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt index aec501d8f..7d9489884 100644 --- a/application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt +++ b/application/core/documents/src/nativeMain/kotlin/io/writeopia/core/folders/di/InDocumentSearchInjection.native.kt @@ -1,9 +1,9 @@ package io.writeopia.core.folders.di -import io.writeopia.core.folders.repository.InDocumentSearchRepository +import io.writeopia.core.folders.repository.InDocumentSearchSqlRepository actual class InDocumentSearchInjection { - actual fun provideInDocumentSearchRepo(): InDocumentSearchRepository { + actual fun provideInDocumentSearchRepo(): InDocumentSearchSqlRepository { TODO("Not yet implemented") } diff --git a/application/core/persistence_room/src/commonMain/kotlin/io/writeopia/persistence/room/WriteopiaAplicationDatabase.kt b/application/core/persistence_room/src/commonMain/kotlin/io/writeopia/persistence/room/WriteopiaAplicationDatabase.kt index db62f507f..d8f2fd45d 100644 --- a/application/core/persistence_room/src/commonMain/kotlin/io/writeopia/persistence/room/WriteopiaAplicationDatabase.kt +++ b/application/core/persistence_room/src/commonMain/kotlin/io/writeopia/persistence/room/WriteopiaAplicationDatabase.kt @@ -37,7 +37,7 @@ expect object AppDatabaseConstructor : RoomDatabaseConstructor Date: Wed, 10 Sep 2025 18:24:44 +0200 Subject: [PATCH 20/39] Trie refactor (#499) Co-authored-by: Leandro Ferreira --- .../ui/edition/TextCommandHandler.kt | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt index c40bd980e..436aba901 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt @@ -170,29 +170,43 @@ class TextCommandHandler( } class Trie { - private class TrieNode { - val children: MutableMap = mutableMapOf() - var isEndOfWord: Boolean = false - } private val root = TrieNode() fun insert(word: String) { - var node = root + var current = root + + word.forEach { ch -> + if (current.children.contains(ch)) { + current = current.children[ch]!! + } else { + current.children[ch] = TrieNode() + current = current.children[ch]!! + } + } - word.forEach { char -> node = node.children.getOrPut(char, ::TrieNode) } - node.isEndOfWord = true + current.isEnd = true } fun search(word: String): Boolean { val node = findNode(word) - return node?.isEndOfWord == true + return node?.isEnd == true } private fun findNode(prefix: String): TrieNode? { - var node = root + var current: TrieNode? = root - prefix.forEach { char -> node = node.children[char] ?: return null } - return node + prefix.asSequence() + .takeWhile { current != null } + .forEach { ch -> + current = current!!.children[ch] + } + + return current } + + private class TrieNode( + val children: MutableMap = mutableMapOf(), + var isEnd: Boolean = false, + ) } From aed6a807a357cafd4f71dbb97a17819d1c133be0 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Mon, 15 Sep 2025 19:49:30 +0200 Subject: [PATCH 21/39] Fixing image reload loop (#500) * Fixing image reload loop * Align images in center --------- Co-authored-by: Leandro Ferreira --- .../kotlin/io/writeopia/ui/components/SwipeBox.kt | 4 +++- .../io/writeopia/ui/drawer/content/ImageDrawer.kt | 14 +++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/SwipeBox.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/SwipeBox.kt index 104cc0768..9ca9308c2 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/SwipeBox.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/SwipeBox.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import kotlin.math.absoluteValue @@ -53,6 +54,7 @@ fun SwipeBox( defaultColor: Color = MaterialTheme.colorScheme.background, activeColor: Color, activeBorderColor: Color = MaterialTheme.colorScheme.primary, + borderWidth: Dp = 1.dp, cornersShape: Shape = MaterialTheme.shapes.medium, swipeListener: (Boolean) -> Unit, content: @Composable BoxScope.() -> Unit @@ -103,7 +105,7 @@ fun SwipeBox( shape = cornersShape ) .border( - width = 1.dp, + width = borderWidth, color = colorBorderAnimated, shape = cornersShape ) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/ImageDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/ImageDrawer.kt index 3f18d7fba..8cee94f37 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/ImageDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/ImageDrawer.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape @@ -88,17 +89,18 @@ class ImageDrawer( val focusRequester = remember { FocusRequester() } SwipeBox( - modifier = Modifier.hoverable(interactionSource), + modifier = Modifier.hoverable(interactionSource).fillMaxWidth(), defaultColor = MaterialTheme.colorScheme.surfaceVariant, activeColor = config.selectedColor(), activeBorderColor = config.selectedBorderColor(), + borderWidth = 3.dp, isOnEditState = drawInfo.selectMode, swipeListener = { isSelected -> onSelected(isSelected, drawInfo.position) } ) { DragCardTarget( - modifier = Modifier.clip(MaterialTheme.shapes.large), + modifier = Modifier.clip(MaterialTheme.shapes.large).align(Alignment.Center), position = drawInfo.position, dataToDrop = dropInfo, iconTintOnHover = MaterialTheme.colorScheme.onBackground, @@ -128,9 +130,11 @@ class ImageDrawer( .build(), contentScale = ContentScale.Crop, contentDescription = "", - modifier = Modifier.clip( - shape = RoundedCornerShape(size = 12.dp) - ), + modifier = Modifier + .defaultMinSize(minHeight = 300.dp) + .clip( + shape = RoundedCornerShape(size = 12.dp) + ), loading = { CircularProgressIndicator() } From bdcec3babc95e0130c950a2a732ca34a91ce867c Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Wed, 17 Sep 2025 18:11:02 +0200 Subject: [PATCH 22/39] UI fixes (#502) * Fixing title erase * fixing commands handling --------- Co-authored-by: Leandro Ferreira --- gradle/libs.versions.toml | 2 +- .../commonMain/kotlin/io/writeopia/sdk/models/story/Tag.kt | 4 ++-- .../kotlin/io/writeopia/ui/edition/TextCommandHandler.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f66c6dafd..cef1b47e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ mockk = "1.14.4" sqldelight = "2.1.0" ksp = "2.2.0-2.0.2" compose-multiplatform = "1.8.0-beta02" -dag-command = "1.11.0" +dag-command = "1.14.0" dokka = "2.0.0" firebase = "33.11.0" google-services = "4.4.2" diff --git a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/Tag.kt b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/Tag.kt index e27306ae1..4b407a4ec 100644 --- a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/Tag.kt +++ b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/story/Tag.kt @@ -29,8 +29,8 @@ enum class Tag(val label: String) { } fun isErasable() = when (this) { - H1, H2, H3, H4, HIDDEN_HX, COLLAPSED, AI_SUGGESTION, FIRST_AI_SUGGESTION -> false - HIGH_LIGHT_BLOCK -> true + HIDDEN_HX, COLLAPSED, AI_SUGGESTION, FIRST_AI_SUGGESTION -> false + H1, H2, H3, H4, HIGH_LIGHT_BLOCK -> true } fun mustCarryOver() = when (this) { diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt index 436aba901..a885308b8 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/edition/TextCommandHandler.kt @@ -22,7 +22,7 @@ class TextCommandHandler( } fun handleCommand(text: String, step: StoryStep, position: Int): Boolean { - if (excludeTypes.contains(step.type.number) || text.lastOrNull() != ' ') return false + if (excludeTypes.contains(step.type.number) || !text.contains(' ')) return false val textArray = text.split(" ") if (textArray.isEmpty()) return false From f39bc561e8deb41c130d6d7016a1cab38195a405 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Thu, 18 Sep 2025 12:53:00 +0200 Subject: [PATCH 23/39] Adding text toolbox (#503) * Adding text toolbox * working textboolbox * adding shadow --------- Co-authored-by: Leandro Ferreira --- .../viewmodel/NoteEditorKmpViewModel.kt | 23 ++- .../editor/viewmodel/NoteEditorViewModel.kt | 2 + .../writeopia/ui/components/EditionScreen.kt | 114 ++++++++++- .../io/writeopia/ui/drawer/TextToolbox.kt | 43 ++++ .../writeopia/ui/drawer/content/TextDrawer.kt | 189 ++++++++++-------- .../ui/drawer/factory/CommonDrawers.kt | 13 +- .../ui/manager/WriteopiaStateManager.kt | 6 + .../drawer/factory/DefaultDrawersDesktop.kt | 18 +- 8 files changed, 310 insertions(+), 98 deletions(-) create mode 100644 writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt index d52fa33ce..23cb8038a 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt @@ -144,6 +144,21 @@ class NoteEditorKmpViewModel( override val showSearchState: StateFlow = _showSearch.asStateFlow() override val searchText: StateFlow = _searchText.asStateFlow() + private val hasLinesSelection = writeopiaManager.onEditPositions + .map { it.isNotEmpty() } + + override val hasSelectedLines: StateFlow = + combine( + hasLinesSelection, + writeopiaManager.textSelectionState + ) { hasLines, selection -> + val hasTextSelection = selection.start != selection.end + + hasLines || hasTextSelection + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + +// val selectionOfText = writeopiaManager. + private val findsOfSearch: Flow> = combine(writeopiaManager.documentInfo, searchText) { info, query -> info.id to query @@ -157,7 +172,7 @@ class NoteEditorKmpViewModel( override val isEditable: StateFlow = writeopiaManager .documentInfo .map { info -> !info.isLocked } - .stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = false) + .stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = false) private val _showGlobalMenu = MutableStateFlow(false) override val showGlobalMenu = _showGlobalMenu.asStateFlow() @@ -192,7 +207,7 @@ class NoteEditorKmpViewModel( override val currentTitle by lazy { writeopiaManager.currentDocument.filterNotNull().map { document -> document.title - }.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = "") + }.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = "") } private val _shouldGoToNextScreen = MutableStateFlow(false) @@ -207,7 +222,7 @@ class NoteEditorKmpViewModel( else -> EditState.TEXT } - }.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = EditState.TEXT) + }.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = EditState.TEXT) } private val story: StateFlow = writeopiaManager.currentStory @@ -229,7 +244,7 @@ class NoteEditorKmpViewModel( override val notFavorite: StateFlow = writeopiaManager .documentInfo .map { info -> !info.isFavorite } - .stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = false) + .stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = false) private var aiJob: Job? = null diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt index 292dd2ce6..23482a250 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt @@ -50,6 +50,8 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { val searchText: StateFlow + val hasSelectedLines: StateFlow + fun showSearch() fun hideSearch() diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt index f48b837de..cb11eab3c 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.writeopia.sdk.models.span.Span import io.writeopia.ui.icons.WrSdkIcons @@ -32,6 +33,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun EditionScreen( modifier: Modifier = Modifier, + iconSize: Dp = 36.dp, onSpanClick: (Span) -> Unit = {}, checkboxClick: () -> Unit = {}, listItemClick: () -> Unit = {}, @@ -43,15 +45,13 @@ fun EditionScreen( ) { val iconPadding = PaddingValues(vertical = 4.dp) val clipShape = MaterialTheme.shapes.medium - val iconSize = 36.dp val spaceWidth = 8.dp - Row(modifier = modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { val tint = MaterialTheme.colorScheme.onPrimary Row( modifier = Modifier - .weight(1f) .horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically ) { @@ -133,7 +133,7 @@ fun EditionScreen( modifier = Modifier .clip(clipShape) .clickable(onClick = onCopy) - .size(32.dp) + .size(iconSize) .padding(iconPadding), imageVector = WrSdkIcons.copy, contentDescription = "Copy", @@ -146,7 +146,7 @@ fun EditionScreen( modifier = Modifier .clip(clipShape) .clickable(onClick = onCut) - .size(32.dp) + .size(iconSize) .padding(iconPadding), imageVector = Icons.Default.ContentCut, contentDescription = "Cut", @@ -195,6 +195,110 @@ fun EditionScreen( } } +@Composable +fun EditionScreenForText( + modifier: Modifier = Modifier, + iconSize: Dp = 36.dp, + onSpanClick: (Span) -> Unit = {}, +) { + val iconPadding = PaddingValues(vertical = 4.dp) + val clipShape = MaterialTheme.shapes.medium + val spaceWidth = 8.dp + + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + val tint = MaterialTheme.colorScheme.onPrimary + + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .clip(clipShape) + .clickable { + onSpanClick(Span.BOLD) + } + .size(iconSize) + .padding(iconPadding), + imageVector = Icons.Outlined.FormatBold, + contentDescription = "BOLD", +// contentDescription = stringResource(R.string.delete), + tint = tint + ) + + Icon( + modifier = Modifier + .clip(clipShape) + .clickable { + onSpanClick(Span.ITALIC) + } + .size(iconSize) + .padding(iconPadding), + imageVector = Icons.Outlined.FormatItalic, + contentDescription = "ITALIC", +// contentDescription = stringResource(R.string.delete), + tint = tint + ) + + Spacer(modifier = Modifier.width(spaceWidth)) + + Icon( + modifier = Modifier + .clip(clipShape) + .clickable { + onSpanClick(Span.UNDERLINE) + } + .size(iconSize) + .padding(iconPadding), + imageVector = Icons.Outlined.FormatUnderlined, + contentDescription = "UNDERLINE", +// contentDescription = stringResource(R.string.delete), + tint = tint + ) + +// Spacer(modifier = Modifier.width(spaceWidth)) +// +// Icon( +// modifier = Modifier +// .clip(clipShape) +// .clickable(onClick = onCopy) +// .size(iconSize) +// .padding(iconPadding), +// imageVector = WrSdkIcons.copy, +// contentDescription = "Copy", +// tint = tint +// ) +// +// Spacer(modifier = Modifier.width(spaceWidth)) +// +// Icon( +// modifier = Modifier +// .clip(clipShape) +// .clickable(onClick = onCut) +// .size(iconSize) +// .padding(iconPadding), +// imageVector = Icons.Default.ContentCut, +// contentDescription = "Cut", +// tint = tint +// ) + +// Spacer(modifier = Modifier.width(spaceWidth)) +// +// Icon( +// modifier = Modifier +// .clip(clipShape) +// .clickable(onClick = onAddPage) +// .size(iconSize) +// .padding(iconPadding), +// imageVector = WrSdkIcons.linkPage, +// contentDescription = "Link to page", +// tint = tint +// ) + } + } +} + @Preview @Composable fun EditionScreenPreview() { diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt new file mode 100644 index 000000000..44cd38fa7 --- /dev/null +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt @@ -0,0 +1,43 @@ +package io.writeopia.ui.drawer + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import io.writeopia.sdk.models.span.Span +import io.writeopia.ui.components.EditionScreenForText + +@Composable +fun TextToolbox( + hasSelection: Boolean, + onSpanClick: (Span) -> Unit = {}, +) { + Popup(offset = IntOffset(0, -40)) { + AnimatedVisibility( + modifier = Modifier, + visible = hasSelection, + enter = fadeIn(animationSpec = tween(durationMillis = 150)), + exit = fadeOut(animationSpec = tween(durationMillis = 150)) + ) { + EditionScreenForText( + modifier = Modifier + .shadow(elevation = 6.dp) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.primary) + .padding(horizontal = 8.dp, vertical = 2.dp), + iconSize = 28.dp, + onSpanClick = onSpanClick, + ) + } + } +} diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt index ebdeb4d9c..ad4fa02f7 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt @@ -3,6 +3,7 @@ package io.writeopia.ui.drawer.content import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions @@ -70,6 +71,7 @@ class TextDrawer( override var onFocusChanged: (Int, FocusState) -> Unit = { _, _ -> }, private val selectionState: StateFlow, private val onSelectionLister: (Int) -> Unit, + private val textToolbox: @Composable (Boolean) -> Unit = {} ) : SimpleTextDrawer { @Composable @@ -106,6 +108,15 @@ class TextDrawer( textLayoutResult?.getLineForOffset(inputText.selection.end) } } + + val selection by remember { + derivedStateOf { + inputText.selection + } + } + + val hasSelection = selection.start != selection.end + val realPosition by remember { derivedStateOf { val lineStart = textLayoutResult?.multiParagraph?.getLineStart(cursorLine ?: 0) @@ -138,95 +149,113 @@ class TextDrawer( val coroutineScope = rememberCoroutineScope() - Row(horizontalArrangement = Arrangement.Center) { - if (isSuggestion) { - BasicTextField( - state = TextFieldState(aiExplanation), - textStyle = textStyle(step).copy(fontStyle = FontStyle.Normal) - ) - } + Box { + Row(horizontalArrangement = Arrangement.Center) { + if (isSuggestion) { + BasicTextField( + state = TextFieldState(aiExplanation), + textStyle = textStyle(step).copy(fontStyle = FontStyle.Normal) + ) + } - BasicTextField( - modifier = modifier - .let { modifierLet -> - if (focusRequester != null) { - modifierLet.focusRequester(focusRequester) - } else { - modifierLet + BasicTextField( + modifier = modifier + .let { modifierLet -> + if (focusRequester != null) { + modifierLet.focusRequester(focusRequester) + } else { + modifierLet + } } - } - .onPreviewKeyEvent { keyEvent -> - onKeyEvent( - keyEvent, - inputText, - step, - drawInfo.position, - emptyErase, - realPosition, - isInLastLine - ) - } - .onFocusChanged { focusState -> - onFocusChanged(drawInfo.position, focusState) - } - .testTag("MessageDrawer_${drawInfo.position}") - .let { modifierLet -> - if (selectionState) { - modifierLet.clickable { onSelectionLister(drawInfo.position) } - } else { - modifierLet + .onPreviewKeyEvent { keyEvent -> + onKeyEvent( + keyEvent, + inputText, + step, + drawInfo.position, + emptyErase, + realPosition, + isInLastLine + ) } + .onFocusChanged { focusState -> + onFocusChanged(drawInfo.position, focusState) + } + .testTag("MessageDrawer_${drawInfo.position}") + .let { modifierLet -> + if (selectionState) { + modifierLet.clickable { onSelectionLister(drawInfo.position) } + } else { + modifierLet + } + }, + value = inputText, + enabled = !selectionState && !drawInfo.selectMode && enabled, + onTextLayout = { + textLayoutResult = it }, - value = inputText, - enabled = !selectionState && !drawInfo.selectMode && enabled, - onTextLayout = { - textLayoutResult = it - }, - onValueChange = { value -> - val start = value.selection.start - val end = value.selection.end - val previousStart = inputText.selection.start - - val sizeDifference = value.text.length - inputText.text.length - - if (abs(sizeDifference) > 0) { - spans = Spans.recalculateSpans(spans, previousStart, sizeDifference) - } - - val edit = { - inputText = value.copy( - Spans.createStringWithSpans( - value.text.replace("\n", ""), - spans, - isDarkTheme + onValueChange = { value -> + val start = value.selection.start + val end = value.selection.end + val previousStart = inputText.selection.start + + val sizeDifference = value.text.length - inputText.text.length + + if (abs(sizeDifference) > 0) { + spans = Spans.recalculateSpans(spans, previousStart, sizeDifference) + } + + val edit = { + inputText = value.copy( + Spans.createStringWithSpans( + value.text.replace("\n", ""), + spans, + isDarkTheme + ) ) - ) - } + } - onTextEdit( - TextInput(value.text, start, end, spans), - drawInfo.position, - lineBreakByContent, - ) + onTextEdit( + TextInput(value.text, start, end, spans), + drawInfo.position, + lineBreakByContent, + ) - if (start == 0 || end == 0) { - coroutineScope.launch { - // Delay to avoid jumping to previous line too soon when erasing text - delay(70) + if (start == 0 || end == 0) { + coroutineScope.launch { + // Delay to avoid jumping to previous line too soon when erasing text + delay(70) + edit() + } + } else { edit() } - } else { - edit() - } - }, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences - ), - textStyle = textStyle(step), - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - interactionSource = interactionSource, - decorationBox = decorationBox, - ) + }, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences + ), + textStyle = textStyle(step), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + interactionSource = interactionSource, + decorationBox = decorationBox, + ) + } + + textToolbox(hasSelection) +// Popup(offset = IntOffset(0, -70)) { +// AnimatedVisibility( +// visible = hasSelection, +// enter = fadeIn(animationSpec = tween(durationMillis = 150)), +// exit = fadeOut(animationSpec = tween(durationMillis = 150)) +// ) { +// EditionScreen( +// modifier = Modifier +// .padding(bottom = 20.dp) +// .clip(MaterialTheme.shapes.large) +// .background(MaterialTheme.colorScheme.primary) +// ) +// } +// } } } } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt index 593a960f6..5ebbce1d7 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt @@ -65,7 +65,8 @@ object CommonDrawers { receiveExternalFile: (List, Int) -> Unit = { _, _ -> }, headerEndContent: @Composable ((StoryStep, DrawInfo, Boolean) -> Unit)? = null, onDocumentLinkClick: (String) -> Unit, - equationToImageUrl: String? = null + equationToImageUrl: String? = null, + textToolbox: @Composable (Boolean) -> Unit = {} ): Map { val commonTextModifier = Modifier.padding( start = drawConfig.codeBlockStartPadding.dp, @@ -85,7 +86,8 @@ object CommonDrawers { enabled = editable, isDarkTheme = isDarkTheme, onSelectionLister = manager::toggleSelection, - textStyle = { defaultTextStyle(it, fontFamily) } + textStyle = { defaultTextStyle(it, fontFamily) }, + textToolbox = textToolbox ) } @@ -121,6 +123,7 @@ object CommonDrawers { isDarkTheme = isDarkTheme, aiExplanation = aiExplanation, onSelectionLister = manager::toggleSelection, + textToolbox = textToolbox ) } ) @@ -316,7 +319,8 @@ private fun RowScope.messageDrawer( isDarkTheme: Boolean, emptyErase: EmptyErase, eventListener: (KeyEvent, TextFieldValue, StoryStep, Int, EmptyErase, Int, EndOfText) -> Boolean, - onSelectionLister: (Int) -> Unit + onSelectionLister: (Int) -> Unit, + textToolbox: @Composable (Boolean) -> Unit ): TextDrawer { return TextDrawer( modifier = modifier.weight(1F), @@ -332,6 +336,7 @@ private fun RowScope.messageDrawer( selectionState = manager.selectionState, onSelectionLister = onSelectionLister, isDarkTheme = isDarkTheme, - aiExplanation = aiExplanation + aiExplanation = aiExplanation, + textToolbox = textToolbox ) } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt index 4780f4ce9..7349c4daa 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt @@ -51,6 +51,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -208,6 +209,11 @@ class WriteopiaStateManager( val currentStory: StateFlow = _currentStory.asStateFlow() + val textSelectionState: Flow = + _currentStory.map { storyState -> + storyState.selection + } + val currentDocument: StateFlow = combine(_documentInfo, _currentStory) { info, state -> parseDocument(info, state) diff --git a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt index 291f196d4..bec702b9b 100644 --- a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt +++ b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import io.writeopia.sdk.models.files.ExternalFile import io.writeopia.sdk.models.story.Tag import io.writeopia.ui.drawer.StoryStepDrawer +import io.writeopia.ui.drawer.TextToolbox import io.writeopia.ui.icons.WrSdkIcons import io.writeopia.ui.manager.WriteopiaStateManager import io.writeopia.ui.model.DrawConfig @@ -45,8 +46,15 @@ object DefaultDrawersDesktop : DrawersFactory { receiveExternalFile: (List, Int) -> Unit, onDocumentLinkClick: (String) -> Unit, equationToImageUrl: String? - ): Map = - CommonDrawers.create( + ): Map { + val textToolbox: @Composable (Boolean) -> Unit = @Composable { hasSelection -> + TextToolbox( + hasSelection = hasSelection, + onSpanClick = manager::toggleSpan, + ) + } + + return CommonDrawers.create( manager, 30.dp, aiExplanation, @@ -57,13 +65,12 @@ object DefaultDrawersDesktop : DrawersFactory { isDesktop = true, isDarkTheme = isDarkTheme, drawConfig = drawConfig, - eventListener = KeyEventListenerFactory.desktop( - manager = manager, - ), + eventListener = KeyEventListenerFactory.desktop(manager = manager), fontFamily = fontFamily, onDocumentLinkClick = onDocumentLinkClick, receiveExternalFile = receiveExternalFile, equationToImageUrl = equationToImageUrl, + textToolbox = textToolbox, headerEndContent = { storyStep, drawInfo, isHovered -> val isTitle = storyStep.tags.any { it.tag.isTitle() } val isCollapsed by lazy { storyStep.tags.any { it.tag == Tag.COLLAPSED } } @@ -121,4 +128,5 @@ object DefaultDrawersDesktop : DrawersFactory { } } ) + } } From e0cdcf61e072addfba7064ebbce2c965d781c94d Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Sat, 27 Sep 2025 15:20:02 +0200 Subject: [PATCH 24/39] Feeback for button in edit menu (#504) * Adding feedback in click for edition menu * removing import * Adding feedback for selection * Code clean --------- Co-authored-by: Leandro Ferreira --- .../ui/desktop/DesktopNoteEditorScreen.kt | 8 +- .../ui/desktop/edit/menu/SideEditorOptions.kt | 458 +++++++++++------- .../viewmodel/NoteEditorKmpViewModel.kt | 14 +- .../editor/viewmodel/NoteEditorViewModel.kt | 3 + .../ui/manager/WriteopiaStateManager.kt | 28 ++ .../writeopia/ui/model/SelectionMetadata.kt | 30 ++ 6 files changed, 353 insertions(+), 188 deletions(-) create mode 100644 writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/model/SelectionMetadata.kt diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt index 661646dd9..740d9570a 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt @@ -138,15 +138,15 @@ fun DesktopNoteEditorScreen( } SideEditorOptions( - modifier = Modifier - .padding(horizontal = 10.dp, vertical = 40.dp) - .align(Alignment.TopEnd), - isDarkTheme, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 40.dp), + alignment = Alignment.TopEnd, + isDarkTheme = isDarkTheme, fontStyleSelected = { noteEditorViewModel.fontFamily }, currentModel = noteEditorViewModel.currentModel, models = noteEditorViewModel.models, isEditableState = noteEditorViewModel.isEditable, isFavorite = noteEditorViewModel.notFavorite, + selectedMetadataState = noteEditorViewModel.selectionMetadataState, boldClick = noteEditorViewModel::onAddSpanClick, setEditable = noteEditorViewModel::toggleEditable, checkItemClick = noteEditorViewModel::onAddCheckListClick, diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt index d4df2389e..526628c76 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt @@ -11,7 +11,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -42,35 +41,38 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties import io.writeopia.common.utils.collections.inBatches import io.writeopia.common.utils.file.fileChooserLoad import io.writeopia.common.utils.file.fileChooserSave import io.writeopia.common.utils.icons.WrIcons -import io.writeopia.common.utils.modifier.clickableWithoutGettingFocus import io.writeopia.model.Font import io.writeopia.resources.WrStrings import io.writeopia.sdk.models.span.Span import io.writeopia.theme.WriteopiaTheme import io.writeopia.ui.icons.WrSdkIcons +import io.writeopia.ui.model.SelectionMetadata import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @Composable fun SideEditorOptions( modifier: Modifier = Modifier, + alignment: Alignment, isDarkTheme: Boolean, currentModel: Flow, models: Flow>, fontStyleSelected: () -> StateFlow, isEditableState: StateFlow, isFavorite: StateFlow, + selectedMetadataState: StateFlow>, boldClick: (Span) -> Unit, setEditable: () -> Unit, checkItemClick: () -> Unit, @@ -104,190 +106,200 @@ fun SideEditorOptions( } } - Row(modifier) { - AnimatedVisibility( - showSubMenu, - enter = fadeIn( - animationSpec = spring(stiffness = Spring.StiffnessHigh) - ) + expandHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessHigh, - visibilityThreshold = IntSize.VisibilityThreshold + Popup( + alignment = alignment, + properties = PopupProperties( + focusable = false, + dismissOnBackPress = false, + dismissOnClickOutside = true + ), + onDismissRequest = { + menuType = OptionsType.NONE + }, + ) { + Row(modifier) { + AnimatedVisibility( + showSubMenu, + enter = fadeIn( + animationSpec = spring(stiffness = Spring.StiffnessHigh) + ) + expandHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessHigh, + visibilityThreshold = IntSize.VisibilityThreshold + ), ), - ), - ) { - Crossfade( - menuType, - animationSpec = tween(200), - modifier = Modifier.pointerInput(Unit) { - detectTapGestures { } - } - ) { type -> - when (type) { - OptionsType.NONE -> {} - - OptionsType.PAGE_STYLE -> { - PageOptions( - changeFontFamily, - isEditableState, - isFavorite, - setEditable, - fontStyleSelected(), - moveToClick, - moveToRoot, - deleteDocument, - toggleFavorite - ) - } + ) { + Crossfade( + menuType, + animationSpec = tween(200), + ) { type -> + when (type) { + OptionsType.NONE -> {} + + OptionsType.PAGE_STYLE -> { + PageOptions( + changeFontFamily, + isEditableState, + isFavorite, + setEditable, + fontStyleSelected(), + moveToClick, + moveToRoot, + deleteDocument, + toggleFavorite + ) + } - OptionsType.TEXT_OPTIONS -> { - TextOptions( - isDarkTheme, - boldClick, - checkItemClick, - listItemClick, - codeBlockClick, - highLightBlockClick, - addImage, - addPage - ) - } + OptionsType.TEXT_OPTIONS -> { + TextOptions( + isDarkTheme, + selectedMetadataState, + boldClick, + checkItemClick, + listItemClick, + codeBlockClick, + highLightBlockClick, + addImage, + addPage + ) + } - OptionsType.EXPORT -> { - Actions( - exportJson, - exportMarkdown, - ) - } + OptionsType.EXPORT -> { + Actions( + exportJson, + exportMarkdown, + ) + } - OptionsType.AI -> { - AiOptions( - currentModel = currentModel, - models = models, - askAiBySelection = askAiBySelection, - aiSummary = aiSummary, - aiActionPoints = aiActionPoints, - aiFaq = aiFaq, - aiTags = aiTags, - selectModel = selectModel - ) + OptionsType.AI -> { + AiOptions( + currentModel = currentModel, + models = models, + askAiBySelection = askAiBySelection, + aiSummary = aiSummary, + aiActionPoints = aiActionPoints, + aiFaq = aiFaq, + aiTags = aiTags, + selectModel = selectModel + ) + } } } } - } - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(4.dp)) - Column( - modifier = Modifier.border( - 1.dp, - MaterialTheme.colorScheme.surfaceVariant, - MaterialTheme.shapes.medium - ).background(MaterialTheme.colorScheme.background, MaterialTheme.shapes.medium) - ) { - val spacing = 3.dp + Column( + modifier = Modifier.border( + 1.dp, + MaterialTheme.colorScheme.surfaceVariant, + MaterialTheme.shapes.medium + ).background(MaterialTheme.colorScheme.background, MaterialTheme.shapes.medium) + ) { + val spacing = 3.dp - Spacer(modifier = Modifier.height(spacing)) + Spacer(modifier = Modifier.height(spacing)) - val background = @Composable { optionsType: OptionsType -> - if (optionsType == menuType) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.background + val background = @Composable { optionsType: OptionsType -> + if (optionsType == menuType) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.background + } } - } - val tint = @Composable { optionsType: OptionsType -> - if (optionsType == menuType) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onBackground + val tint = @Composable { optionsType: OptionsType -> + if (optionsType == menuType) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + } } - } - Icon( - imageVector = WrIcons.pageStyle, - contentDescription = "Document Style", - modifier = Modifier - .padding(horizontal = spacing) - .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.PAGE_STYLE)) - .clickable { - menuType = if (menuType != OptionsType.PAGE_STYLE) { - OptionsType.PAGE_STYLE - } else { - OptionsType.NONE + Icon( + imageVector = WrIcons.pageStyle, + contentDescription = "Document Style", + modifier = Modifier + .padding(horizontal = spacing) + .clip(MaterialTheme.shapes.medium) + .background(background(OptionsType.PAGE_STYLE)) + .clickable { + menuType = if (menuType != OptionsType.PAGE_STYLE) { + OptionsType.PAGE_STYLE + } else { + OptionsType.NONE + } } - } - .size(40.dp) - .padding(10.dp), - tint = tint(OptionsType.PAGE_STYLE) - ) + .size(40.dp) + .padding(10.dp), + tint = tint(OptionsType.PAGE_STYLE) + ) - Spacer(modifier = Modifier.height(1.dp)) - - Icon( - imageVector = WrIcons.textStyle, - contentDescription = "Font Style", - modifier = Modifier - .padding(horizontal = spacing) - .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.TEXT_OPTIONS)) - .clickable { - menuType = if (menuType != OptionsType.TEXT_OPTIONS) { - OptionsType.TEXT_OPTIONS - } else { - OptionsType.NONE + Spacer(modifier = Modifier.height(1.dp)) + + Icon( + imageVector = WrIcons.textStyle, + contentDescription = "Font Style", + modifier = Modifier + .padding(horizontal = spacing) + .clip(MaterialTheme.shapes.medium) + .background(background(OptionsType.TEXT_OPTIONS)) + .clickable { + menuType = if (menuType != OptionsType.TEXT_OPTIONS) { + OptionsType.TEXT_OPTIONS + } else { + OptionsType.NONE + } } - } - .size(40.dp) - .padding(9.dp), - tint = tint(OptionsType.TEXT_OPTIONS) - ) + .size(40.dp) + .padding(9.dp), + tint = tint(OptionsType.TEXT_OPTIONS) + ) - Icon( - imageVector = WrIcons.ai, - contentDescription = "AI", - modifier = Modifier - .padding(horizontal = spacing) - .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.AI)) - .clickable { - menuType = if (menuType != OptionsType.AI) { - OptionsType.AI - } else { - OptionsType.NONE + Icon( + imageVector = WrIcons.ai, + contentDescription = "AI", + modifier = Modifier + .padding(horizontal = spacing) + .clip(MaterialTheme.shapes.medium) + .background(background(OptionsType.AI)) + .clickable { + menuType = if (menuType != OptionsType.AI) { + OptionsType.AI + } else { + OptionsType.NONE + } } - } - .size(40.dp) - .padding(9.dp), - tint = tint(OptionsType.AI) - ) + .size(40.dp) + .padding(9.dp), + tint = tint(OptionsType.AI) + ) - Spacer(modifier = Modifier.height(spacing)) - - Spacer(modifier = Modifier.height(spacing)) - - Icon( - imageVector = WrIcons.exportFile, - contentDescription = "Export file", - modifier = Modifier - .padding(horizontal = spacing) - .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.EXPORT)) - .clickable { - menuType = if (menuType != OptionsType.EXPORT) { - OptionsType.EXPORT - } else { - OptionsType.NONE + Spacer(modifier = Modifier.height(spacing)) + + Spacer(modifier = Modifier.height(spacing)) + + Icon( + imageVector = WrIcons.exportFile, + contentDescription = "Export file", + modifier = Modifier + .padding(horizontal = spacing) + .clip(MaterialTheme.shapes.medium) + .background(background(OptionsType.EXPORT)) + .clickable { + menuType = if (menuType != OptionsType.EXPORT) { + OptionsType.EXPORT + } else { + OptionsType.NONE + } } - } - .size(40.dp) - .padding(9.dp), - tint = tint(OptionsType.EXPORT) - ) + .size(40.dp) + .padding(9.dp), + tint = tint(OptionsType.EXPORT) + ) - Spacer(modifier = Modifier.height(spacing)) + Spacer(modifier = Modifier.height(spacing)) + } } } } @@ -366,9 +378,7 @@ private fun TextChanges(spanClick: (Span) -> Unit) { modifier = Modifier.weight(1F) .clip(RoundedCornerShape(topStart = 6.dp, bottomStart = 6.dp)) .size(32.dp) - .clickableWithoutGettingFocus { - spanClick(Span.BOLD) - } + .clickable { spanClick(Span.BOLD) } .padding(horizontal = 8.dp, vertical = 8.dp), tint = MaterialTheme.colorScheme.onBackground ) @@ -378,7 +388,7 @@ private fun TextChanges(spanClick: (Span) -> Unit) { contentDescription = "Italic", modifier = Modifier.weight(1F) .size(32.dp) - .clickableWithoutGettingFocus { spanClick(Span.ITALIC) } + .clickable { spanClick(Span.ITALIC) } .padding(horizontal = 8.dp, vertical = 8.dp), tint = MaterialTheme.colorScheme.onBackground ) @@ -389,7 +399,7 @@ private fun TextChanges(spanClick: (Span) -> Unit) { modifier = Modifier.weight(1F) .clip(RoundedCornerShape(topEnd = 6.dp, bottomEnd = 6.dp)) .size(32.dp) - .clickableWithoutGettingFocus { spanClick(Span.UNDERLINE) } + .clickable { spanClick(Span.UNDERLINE) } .padding(horizontal = 8.dp, vertical = 8.dp), tint = MaterialTheme.colorScheme.onBackground ) @@ -421,18 +431,14 @@ private fun HighlightText(isDarkTheme: Boolean, spanClick: (Span) -> Unit) { Box( modifier = Modifier.weight(1F) .size(32.dp) - .clickableWithoutGettingFocus { - spanClick(span) - }, + .clickable { spanClick(span) }, ) { Box( modifier = Modifier.size(16.dp) .align(Alignment.Center) .background(color, CircleShape) .clip(CircleShape) - .clickableWithoutGettingFocus { - spanClick(span) - } + .clickable { spanClick(span) } ) } } @@ -473,15 +479,16 @@ private fun IconAndText(text: String, iconImage: ImageVector, click: () -> Unit) } @Composable -private fun DecorationCommands(commands: Iterable Unit>>) { +private fun DecorationCommands(commands: Iterable) { Column { commands.inBatches(2) .forEach { line -> Row { - line.forEach { (command, listener) -> + line.forEach { (command, highlight, listener) -> TextButton( modifier = Modifier.weight(1F), text = command, + highlight = highlight, onClick = listener ) } @@ -494,20 +501,36 @@ private fun DecorationCommands(commands: Iterable Unit>>) { private fun TextButton( modifier: Modifier = Modifier, text: String, + highlight: Boolean = false, paddingValues: PaddingValues = PaddingValues( horizontal = 8.dp, vertical = 8.dp ), onClick: () -> Unit, ) { + val shape = MaterialTheme.shapes.medium + Text( modifier = modifier .padding(start = 2.dp, end = 2.dp, bottom = 3.dp) - .clip(MaterialTheme.shapes.medium) + .clip(shape) .clickable(onClick = onClick) + .border( + width = 1.dp, + color = if (highlight) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + shape = shape + ) .background( - MaterialTheme.colorScheme.surfaceVariant, - MaterialTheme.shapes.medium + color = if (highlight) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + shape = shape ) .padding(paddingValues), text = text, @@ -520,15 +543,37 @@ private fun TextButton( @Composable private fun InsertCommand( + selectedMetadataState: StateFlow>, checkItemClick: () -> Unit, listItemClick: () -> Unit, codeBlockClick: () -> Unit, ) { + val selectedMetadata by selectedMetadataState.collectAsState() + Row(modifier = Modifier.horizontalOptionsRow()) { + val shapeLeft = RoundedCornerShape(topStart = 6.dp, bottomStart = 6.dp) + Icon( imageVector = WrSdkIcons.checkbox, contentDescription = "Check box", modifier = Modifier.weight(1F) + .border( + width = 1.dp, + shape = shapeLeft, + color = if (selectedMetadata.contains(SelectionMetadata.CHECK_ITEM)) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + Color.Transparent + } + ) + .background( + color = if (selectedMetadata.contains(SelectionMetadata.CHECK_ITEM)) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + Color.Transparent + }, + shape = shapeLeft + ) .clip(RoundedCornerShape(topStart = 6.dp, bottomStart = 6.dp)) .size(32.dp) .clickable(onClick = checkItemClick) @@ -540,17 +585,51 @@ private fun InsertCommand( imageVector = WrSdkIcons.list, contentDescription = "List item", modifier = Modifier.weight(1F) + .border( + width = 1.dp, + color = if (selectedMetadata.contains(SelectionMetadata.UNORDERED_LIST_ITEM)) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + Color.Transparent + } + ) + .background( + color = if (selectedMetadata.contains(SelectionMetadata.UNORDERED_LIST_ITEM)) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + Color.Transparent + }, + ) .size(32.dp) .clickable(onClick = listItemClick) .padding(horizontal = 8.dp, vertical = 6.dp), tint = MaterialTheme.colorScheme.onBackground ) + val shapeRight = RoundedCornerShape(topEnd = 6.dp, bottomEnd = 6.dp) + Icon( imageVector = WrIcons.code, contentDescription = "Code block", modifier = Modifier.weight(1F) - .clip(RoundedCornerShape(topEnd = 6.dp, bottomEnd = 6.dp)) + .border( + width = 1.dp, + shape = shapeRight, + color = if (selectedMetadata.contains(SelectionMetadata.CODE_BLOCK)) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + Color.Transparent + } + ) + .background( + color = if (selectedMetadata.contains(SelectionMetadata.CODE_BLOCK)) { + WriteopiaTheme.colorScheme.optionsSelector + } else { + Color.Transparent + }, + shape = shapeRight + ) + .clip(shapeRight) .size(32.dp) .clickable(onClick = codeBlockClick) .padding(horizontal = 8.dp, vertical = 7.dp), @@ -612,6 +691,7 @@ internal fun FontOptions( @Composable private fun TextOptions( isDarkTheme: Boolean, + selectedMetadataState: StateFlow>, spanClick: (Span) -> Unit, checkItemClick: () -> Unit, listItemClick: () -> Unit, @@ -642,15 +722,21 @@ private fun TextOptions( Title(WrStrings.insert()) Spacer(modifier = Modifier.height(4.dp)) - InsertCommand(checkItemClick, listItemClick, codeBlockClick) + InsertCommand(selectedMetadataState, checkItemClick, listItemClick, codeBlockClick) Spacer(modifier = Modifier.height(8.dp)) Title(WrStrings.decoration()) Spacer(modifier = Modifier.height(4.dp)) + val selectedMetadata by selectedMetadataState.collectAsState() + DecorationCommands( commands = listOf( - WrStrings.box() to highLightBlockClick, + DecorationButton( + text = WrStrings.box(), + highlight = selectedMetadata.contains(SelectionMetadata.BOX), + onClick = highLightBlockClick + ) // "Warning" to {}, // "Tip" to {}, // "Code" to {} @@ -846,3 +932,9 @@ private enum class OptionsType { EXPORT, AI } + +private data class DecorationButton( + val text: String, + val highlight: Boolean, + val onClick: () -> Unit +) diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt index 23cb8038a..fb75fa1f7 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt @@ -44,6 +44,7 @@ import io.writeopia.ui.backstack.BackstackInform import io.writeopia.ui.keyboard.KeyboardEvent import io.writeopia.ui.manager.WriteopiaStateManager import io.writeopia.ui.model.DrawState +import io.writeopia.ui.model.SelectionMetadata import io.writeopia.ui.utils.Spans import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -177,6 +178,13 @@ class NoteEditorKmpViewModel( private val _showGlobalMenu = MutableStateFlow(false) override val showGlobalMenu = _showGlobalMenu.asStateFlow() + override val selectionMetadataState: StateFlow> = + writeopiaManager.selectionMetadataState.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + emptySet() + ) + @OptIn(ExperimentalCoroutinesApi::class) private val aiConfigState = authRepository.listenForUser() .flatMapConcat { user -> @@ -222,7 +230,11 @@ class NoteEditorKmpViewModel( else -> EditState.TEXT } - }.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = EditState.TEXT) + }.stateIn( + viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = EditState.TEXT + ) } private val story: StateFlow = writeopiaManager.currentStory diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt index 23482a250..fbf06f6d4 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt @@ -9,6 +9,7 @@ import io.writeopia.ui.backstack.BackstackHandler import io.writeopia.ui.backstack.BackstackInform import io.writeopia.ui.manager.WriteopiaStateManager import io.writeopia.ui.model.DrawState +import io.writeopia.ui.model.SelectionMetadata import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -52,6 +53,8 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { val hasSelectedLines: StateFlow + val selectionMetadataState: StateFlow> + fun showSearch() fun hideSearch() diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt index 7349c4daa..f7ba25665 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt @@ -37,6 +37,7 @@ import io.writeopia.ui.keyboard.KeyboardEvent import io.writeopia.ui.model.DrawState import io.writeopia.ui.model.DrawStory import io.writeopia.ui.model.SelectionInfo +import io.writeopia.ui.model.SelectionMetadata import io.writeopia.ui.model.TextInput import io.writeopia.ui.modifiers.StepsModifier import kotlinx.coroutines.CoroutineDispatcher @@ -248,6 +249,33 @@ class WriteopiaStateManager( private var _initialized = false + val selectionMetadataState: Flow> = _currentStory.map { storyState -> + val selection = storyState.selection.position + val step = storyState.stories[selection] + + if (step == null) { + emptySet() + } else { + val fromType = SelectionMetadata.fromStoryType(step.type.number) + val result = mutableSetOf() + + if (fromType != null) { + result.add(fromType) + } + + step.tags.forEach { tagInfo -> + when (tagInfo.tag) { + Tag.HIGH_LIGHT_BLOCK -> { + result.add(SelectionMetadata.BOX) + } + else -> {} + } + } + + result + } + } + val isOnSelection: Boolean get() = _positionsOnEdit.value.isNotEmpty() diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/model/SelectionMetadata.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/model/SelectionMetadata.kt new file mode 100644 index 000000000..a2fa44cc1 --- /dev/null +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/model/SelectionMetadata.kt @@ -0,0 +1,30 @@ +package io.writeopia.ui.model + +import io.writeopia.sdk.models.story.StoryTypes + +enum class SelectionMetadata { + BOLD, + ITALIC, + UNDERLINE, + TITLE, + SUBTITLE, + HEADING, + BOX, + CHECK_ITEM, + CODE_BLOCK, + UNORDERED_LIST_ITEM, + DOCUMENT_LINK; + + companion object { + fun fromStoryType(storyTypeNumer: Int): SelectionMetadata? { + val storyType = StoryTypes.fromNumber(storyTypeNumer) + + return when (storyType) { + StoryTypes.CHECK_ITEM -> CHECK_ITEM + StoryTypes.UNORDERED_LIST_ITEM -> UNORDERED_LIST_ITEM + StoryTypes.DOCUMENT_LINK -> DOCUMENT_LINK + else -> null + } + } + } +} From 8411c747654d24a69ac28cefaf09c62238ed4260 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Sat, 27 Sep 2025 20:52:26 +0200 Subject: [PATCH 25/39] Hiding the side menu when user clicks outside of the menu (#505) Co-authored-by: Leandro Ferreira --- .../ui/desktop/DesktopNoteEditorScreen.kt | 13 ++- .../ui/desktop/edit/menu/SideEditorOptions.kt | 84 +++++++++---------- .../viewmodel/NoteEditorKmpViewModel.kt | 7 ++ .../editor/viewmodel/NoteEditorViewModel.kt | 12 +++ 4 files changed, 72 insertions(+), 44 deletions(-) diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt index 740d9570a..a911960c2 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt @@ -40,6 +40,7 @@ import io.writeopia.commonui.dialogs.confirmation.DeleteConfirmationDialog import io.writeopia.editor.features.editor.ui.desktop.edit.menu.SideEditorOptions import io.writeopia.editor.features.editor.ui.folders.FolderSelectionDialog import io.writeopia.editor.features.editor.viewmodel.NoteEditorViewModel +import io.writeopia.editor.features.editor.viewmodel.SideMenuTab import io.writeopia.theme.WriteopiaTheme import io.writeopia.ui.drawer.factory.DrawersFactory @@ -62,7 +63,10 @@ fun DesktopNoteEditorScreen( Box( modifier = Modifier.clickable( - onClick = noteEditorViewModel::clearSelections, + onClick = { + noteEditorViewModel.clearSelections() + noteEditorViewModel.changeSideMenu(SideMenuTab.NONE) + }, interactionSource = interactionSource, indication = null ) @@ -81,6 +85,11 @@ fun DesktopNoteEditorScreen( loadNoteId = documentId, onDocumentLinkClick = onDocumentLinkClick, modifier = Modifier.padding(start = 30.dp, end = 30.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { } + ) ) } ) @@ -147,6 +156,7 @@ fun DesktopNoteEditorScreen( isEditableState = noteEditorViewModel.isEditable, isFavorite = noteEditorViewModel.notFavorite, selectedMetadataState = noteEditorViewModel.selectionMetadataState, + sideMenuTabState = noteEditorViewModel.sideMenuTabState, boldClick = noteEditorViewModel::onAddSpanClick, setEditable = noteEditorViewModel::toggleEditable, checkItemClick = noteEditorViewModel::onAddCheckListClick, @@ -173,6 +183,7 @@ fun DesktopNoteEditorScreen( aiFaq = noteEditorViewModel::aiFaq, aiTags = noteEditorViewModel::aiTags, selectModel = noteEditorViewModel::selectModel, + changeSideMenuTab = noteEditorViewModel::changeSideMenu ) if (showDeleteConfirmation) { diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt index 526628c76..f99409562 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt @@ -53,6 +53,7 @@ import io.writeopia.common.utils.collections.inBatches import io.writeopia.common.utils.file.fileChooserLoad import io.writeopia.common.utils.file.fileChooserSave import io.writeopia.common.utils.icons.WrIcons +import io.writeopia.editor.features.editor.viewmodel.SideMenuTab import io.writeopia.model.Font import io.writeopia.resources.WrStrings import io.writeopia.sdk.models.span.Span @@ -73,6 +74,7 @@ fun SideEditorOptions( isEditableState: StateFlow, isFavorite: StateFlow, selectedMetadataState: StateFlow>, + sideMenuTabState: StateFlow, boldClick: (Span) -> Unit, setEditable: () -> Unit, checkItemClick: () -> Unit, @@ -95,14 +97,13 @@ fun SideEditorOptions( deleteDocument: () -> Unit, toggleFavorite: () -> Unit, selectModel: (String) -> Unit, + changeSideMenuTab: (SideMenuTab) -> Unit ) { - var menuType by remember { - mutableStateOf(OptionsType.NONE) - } + val menuType by sideMenuTabState.collectAsState() val showSubMenu by remember { derivedStateOf { - menuType != OptionsType.NONE + menuType != SideMenuTab.NONE } } @@ -111,11 +112,8 @@ fun SideEditorOptions( properties = PopupProperties( focusable = false, dismissOnBackPress = false, - dismissOnClickOutside = true + dismissOnClickOutside = false ), - onDismissRequest = { - menuType = OptionsType.NONE - }, ) { Row(modifier) { AnimatedVisibility( @@ -134,9 +132,9 @@ fun SideEditorOptions( animationSpec = tween(200), ) { type -> when (type) { - OptionsType.NONE -> {} + SideMenuTab.NONE -> {} - OptionsType.PAGE_STYLE -> { + SideMenuTab.PAGE_STYLE -> { PageOptions( changeFontFamily, isEditableState, @@ -150,7 +148,7 @@ fun SideEditorOptions( ) } - OptionsType.TEXT_OPTIONS -> { + SideMenuTab.TEXT_OPTIONS -> { TextOptions( isDarkTheme, selectedMetadataState, @@ -164,14 +162,14 @@ fun SideEditorOptions( ) } - OptionsType.EXPORT -> { + SideMenuTab.EXPORT -> { Actions( exportJson, exportMarkdown, ) } - OptionsType.AI -> { + SideMenuTab.AI -> { AiOptions( currentModel = currentModel, models = models, @@ -200,7 +198,7 @@ fun SideEditorOptions( Spacer(modifier = Modifier.height(spacing)) - val background = @Composable { optionsType: OptionsType -> + val background = @Composable { optionsType: SideMenuTab -> if (optionsType == menuType) { MaterialTheme.colorScheme.primary } else { @@ -208,7 +206,7 @@ fun SideEditorOptions( } } - val tint = @Composable { optionsType: OptionsType -> + val tint = @Composable { optionsType: SideMenuTab -> if (optionsType == menuType) { MaterialTheme.colorScheme.onPrimary } else { @@ -222,17 +220,19 @@ fun SideEditorOptions( modifier = Modifier .padding(horizontal = spacing) .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.PAGE_STYLE)) + .background(background(SideMenuTab.PAGE_STYLE)) .clickable { - menuType = if (menuType != OptionsType.PAGE_STYLE) { - OptionsType.PAGE_STYLE + val menuType = if (menuType != SideMenuTab.PAGE_STYLE) { + SideMenuTab.PAGE_STYLE } else { - OptionsType.NONE + SideMenuTab.NONE } + + changeSideMenuTab(menuType) } .size(40.dp) .padding(10.dp), - tint = tint(OptionsType.PAGE_STYLE) + tint = tint(SideMenuTab.PAGE_STYLE) ) Spacer(modifier = Modifier.height(1.dp)) @@ -243,17 +243,19 @@ fun SideEditorOptions( modifier = Modifier .padding(horizontal = spacing) .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.TEXT_OPTIONS)) + .background(background(SideMenuTab.TEXT_OPTIONS)) .clickable { - menuType = if (menuType != OptionsType.TEXT_OPTIONS) { - OptionsType.TEXT_OPTIONS + val menuType = if (menuType != SideMenuTab.TEXT_OPTIONS) { + SideMenuTab.TEXT_OPTIONS } else { - OptionsType.NONE + SideMenuTab.NONE } + + changeSideMenuTab(menuType) } .size(40.dp) .padding(9.dp), - tint = tint(OptionsType.TEXT_OPTIONS) + tint = tint(SideMenuTab.TEXT_OPTIONS) ) Icon( @@ -262,17 +264,19 @@ fun SideEditorOptions( modifier = Modifier .padding(horizontal = spacing) .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.AI)) + .background(background(SideMenuTab.AI)) .clickable { - menuType = if (menuType != OptionsType.AI) { - OptionsType.AI + val menuType = if (menuType != SideMenuTab.AI) { + SideMenuTab.AI } else { - OptionsType.NONE + SideMenuTab.NONE } + + changeSideMenuTab(menuType) } .size(40.dp) .padding(9.dp), - tint = tint(OptionsType.AI) + tint = tint(SideMenuTab.AI) ) Spacer(modifier = Modifier.height(spacing)) @@ -285,17 +289,19 @@ fun SideEditorOptions( modifier = Modifier .padding(horizontal = spacing) .clip(MaterialTheme.shapes.medium) - .background(background(OptionsType.EXPORT)) + .background(background(SideMenuTab.EXPORT)) .clickable { - menuType = if (menuType != OptionsType.EXPORT) { - OptionsType.EXPORT + val menuType = if (menuType != SideMenuTab.EXPORT) { + SideMenuTab.EXPORT } else { - OptionsType.NONE + SideMenuTab.NONE } + + changeSideMenuTab(menuType) } .size(40.dp) .padding(9.dp), - tint = tint(OptionsType.EXPORT) + tint = tint(SideMenuTab.EXPORT) ) Spacer(modifier = Modifier.height(spacing)) @@ -925,14 +931,6 @@ private fun buttonsTextStyle() = @Composable private fun smallButtonPadding() = PaddingValues(horizontal = 8.dp, vertical = 4.dp) -private enum class OptionsType { - NONE, - PAGE_STYLE, - TEXT_OPTIONS, - EXPORT, - AI -} - private data class DecorationButton( val text: String, val highlight: Boolean, diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt index fb75fa1f7..5c3859f1a 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt @@ -250,6 +250,9 @@ class NoteEditorKmpViewModel( .stateIn(viewModelScope, SharingStarted.Lazily, "") } + private val _sideMenuTab = MutableStateFlow(SideMenuTab.NONE) + override val sideMenuTabState: StateFlow = _sideMenuTab.asStateFlow() + /** * This property defines if the document is favorite */ @@ -358,6 +361,10 @@ class NoteEditorKmpViewModel( } }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + override fun changeSideMenu(tab: SideMenuTab) { + _sideMenuTab.value = tab + } + override fun deleteSelection() { writeopiaManager.deleteSelection() } diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt index fbf06f6d4..83c66b5f7 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt @@ -55,6 +55,10 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { val selectionMetadataState: StateFlow> + val sideMenuTabState: StateFlow + + fun changeSideMenu(tab: SideMenuTab) + fun showSearch() fun hideSearch() @@ -141,3 +145,11 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { } data class ShareDocument(val content: String, val title: String, val type: String) + +public enum class SideMenuTab { + NONE, + PAGE_STYLE, + TEXT_OPTIONS, + EXPORT, + AI +} From cf6a4e40d4a967388dcae652beb154116c5ac0b6 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Sat, 27 Sep 2025 22:11:17 +0200 Subject: [PATCH 26/39] Adding title buttons (#506) Co-authored-by: Leandro Ferreira --- .../ui/desktop/DesktopNoteEditorScreen.kt | 3 +- .../ui/desktop/edit/menu/SideEditorOptions.kt | 75 +++++++++++++++++-- .../viewmodel/NoteEditorKmpViewModel.kt | 7 ++ .../editor/viewmodel/NoteEditorViewModel.kt | 3 + .../ui/manager/WriteopiaStateManager.kt | 42 ++++++++++- 5 files changed, 119 insertions(+), 11 deletions(-) diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt index a911960c2..364c9496a 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt @@ -183,7 +183,8 @@ fun DesktopNoteEditorScreen( aiFaq = noteEditorViewModel::aiFaq, aiTags = noteEditorViewModel::aiTags, selectModel = noteEditorViewModel::selectModel, - changeSideMenuTab = noteEditorViewModel::changeSideMenu + changeSideMenuTab = noteEditorViewModel::changeSideMenu, + titleClick = noteEditorViewModel::titleClick ) if (showDeleteConfirmation) { diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt index f99409562..4b589e36b 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -57,6 +58,7 @@ import io.writeopia.editor.features.editor.viewmodel.SideMenuTab import io.writeopia.model.Font import io.writeopia.resources.WrStrings import io.writeopia.sdk.models.span.Span +import io.writeopia.sdk.models.story.Tag import io.writeopia.theme.WriteopiaTheme import io.writeopia.ui.icons.WrSdkIcons import io.writeopia.ui.model.SelectionMetadata @@ -97,7 +99,8 @@ fun SideEditorOptions( deleteDocument: () -> Unit, toggleFavorite: () -> Unit, selectModel: (String) -> Unit, - changeSideMenuTab: (SideMenuTab) -> Unit + changeSideMenuTab: (SideMenuTab) -> Unit, + titleClick: (Tag) -> Unit ) { val menuType by sideMenuTabState.collectAsState() @@ -158,7 +161,8 @@ fun SideEditorOptions( codeBlockClick, highLightBlockClick, addImage, - addPage + addPage, + titleClick ) } @@ -372,6 +376,57 @@ private fun Title(text: String) { ) } +@Composable +private fun TitleChanges( + metadata: Set, + modifier: Modifier = Modifier, + titleClick: (Tag) -> Unit +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + modifier = Modifier.weight(1F), + text = "Title", + highlight = metadata.contains( + SelectionMetadata.TITLE + ), + textStyle = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Normal, + onClick = { + titleClick(Tag.H1) + } + ) + + TextButton( + modifier = Modifier.weight(1F), + text = "Subtitle", + highlight = metadata.contains( + SelectionMetadata.SUBTITLE + ), + textStyle = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Normal, + onClick = { + titleClick(Tag.H2) + } + ) + + TextButton( + modifier = Modifier.weight(1F), + text = "Heading", + highlight = metadata.contains( + SelectionMetadata.HEADING + ), + textStyle = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Normal, + onClick = { + titleClick(Tag.H3) + } + ) + } +} + @Composable private fun TextChanges(spanClick: (Span) -> Unit) { Row( @@ -512,6 +567,8 @@ private fun TextButton( horizontal = 8.dp, vertical = 8.dp ), + textStyle: TextStyle = buttonsTextStyle(), + fontWeight: FontWeight = FontWeight.Bold, onClick: () -> Unit, ) { val shape = MaterialTheme.shapes.medium @@ -541,8 +598,8 @@ private fun TextButton( .padding(paddingValues), text = text, color = MaterialTheme.colorScheme.onBackground, - style = buttonsTextStyle(), - fontWeight = FontWeight.Bold, + style = textStyle, + fontWeight = fontWeight, textAlign = TextAlign.Center ) } @@ -705,6 +762,7 @@ private fun TextOptions( highLightBlockClick: () -> Unit, addImage: (String) -> Unit, addPage: () -> Unit, + titleClick: (Tag) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -716,6 +774,13 @@ private fun TextOptions( .width(250.dp) .padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 12.dp) ) { + val selectedMetadata by selectedMetadataState.collectAsState() + + Title("Title") + Spacer(modifier = Modifier.height(4.dp)) + TitleChanges(metadata = selectedMetadata, titleClick = titleClick) + Spacer(modifier = Modifier.height(8.dp)) + Title(WrStrings.text()) Spacer(modifier = Modifier.height(4.dp)) TextChanges(spanClick) @@ -734,8 +799,6 @@ private fun TextOptions( Title(WrStrings.decoration()) Spacer(modifier = Modifier.height(4.dp)) - val selectedMetadata by selectedMetadataState.collectAsState() - DecorationCommands( commands = listOf( DecorationButton( diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt index 5c3859f1a..53d49f8b7 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt @@ -31,6 +31,7 @@ import io.writeopia.sdk.models.id.GenerateId import io.writeopia.sdk.models.span.Span import io.writeopia.sdk.models.span.SpanInfo import io.writeopia.sdk.models.story.StoryTypes +import io.writeopia.sdk.models.story.Tag import io.writeopia.sdk.models.utils.ResultData import io.writeopia.sdk.persistence.core.tracker.OnUpdateDocumentTracker import io.writeopia.sdk.repository.DocumentRepository @@ -724,6 +725,12 @@ class NoteEditorKmpViewModel( _searchText.value = query } + override fun titleClick(tag: Tag) { + viewModelScope.launch(Dispatchers.Default) { + writeopiaManager.addTitle(tag) + } + } + private fun documentPrompt(promptFn: (String, String, String) -> Flow>) { if (ollamaRepository == null) return diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt index 83c66b5f7..d71c71f04 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt @@ -5,6 +5,7 @@ import io.writeopia.editor.model.EditState import io.writeopia.model.Font import io.writeopia.sdk.models.files.ExternalFile import io.writeopia.sdk.models.span.Span +import io.writeopia.sdk.models.story.Tag import io.writeopia.ui.backstack.BackstackHandler import io.writeopia.ui.backstack.BackstackInform import io.writeopia.ui.manager.WriteopiaStateManager @@ -142,6 +143,8 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { fun setTheme(isDarkTheme: Boolean) fun selectModel(model: String) + + fun titleClick(tag: Tag) } data class ShareDocument(val content: String, val title: String, val type: String) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt index f7ba25665..768680acd 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt @@ -268,6 +268,19 @@ class WriteopiaStateManager( Tag.HIGH_LIGHT_BLOCK -> { result.add(SelectionMetadata.BOX) } + + Tag.H1 -> { + result.add(SelectionMetadata.TITLE) + } + + Tag.H2 -> { + result.add(SelectionMetadata.SUBTITLE) + } + + Tag.H3 -> { + result.add(SelectionMetadata.HEADING) + } + else -> {} } } @@ -1127,6 +1140,26 @@ class WriteopiaStateManager( } } + fun addTitle(tag: Tag) { + val position = currentPosition() + + val currentStory = getStory(position) + if (currentStory == null) return + + val shouldRemove = currentStory.tags.any { it.tag == tag } + + val newTags = currentStory.tags + .filterNotTo(mutableSetOf()) { it.tag.isTitle() } + .apply { + if (!shouldRemove) { + add(TagInfo(tag)) + } + } + + val newStory = currentStory.copy(tags = newTags) + changeStoryState(Action.StoryStateChange(newStory, position)) + } + private suspend fun getUserId(): String = userRepository?.getUser()?.id ?: WriteopiaUser.disconnectedUser().id @@ -1292,8 +1325,8 @@ class WriteopiaStateManager( _currentStory.value = state.copy( selection = Selection( - stateChange.selectionStart ?: 0, - stateChange.selectionEnd ?: 0, + stateChange.selectionStart ?: state.selection.start, + stateChange.selectionEnd ?: state.selection.end, stateChange.position ) ) @@ -1304,9 +1337,10 @@ class WriteopiaStateManager( private fun getStories() = _currentStory.value.stories - private fun currentPosition() = _currentStory.value.focus + private fun currentPosition() = + _currentStory.value.focus ?: _currentStory.value.selection.position - private fun getCurrentStory(): StoryStep? = currentPosition()?.let(::getStory) + private fun getCurrentStory(): StoryStep? = currentPosition().let(::getStory) private fun selectAll() { _positionsOnEdit.value = getStories().keys - setOf(0) From 049b4842c785634691a498c92ff2af49a73e7696 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Sun, 12 Oct 2025 13:23:14 +0200 Subject: [PATCH 27/39] Translations for titles (#507) * translations for titles * baseline for resources --------- Co-authored-by: Leandro Ferreira --- .../core/resources/config/ktlint/baseline.xml | 8138 +++++++++-------- .../composeResources/values-en/strings.xml | 3 + .../composeResources/values-pt/strings.xml | 3 + .../composeResources/values/strings.xml | 3 + .../io/writeopia/resources/WrStrings.kt | 12 + .../ui/desktop/edit/menu/SideEditorOptions.kt | 6 +- 6 files changed, 4156 insertions(+), 4009 deletions(-) diff --git a/application/core/resources/config/ktlint/baseline.xml b/application/core/resources/config/ktlint/baseline.xml index 7b78cd944..bdee5362d 100644 --- a/application/core/resources/config/ktlint/baseline.xml +++ b/application/core/resources/config/ktlint/baseline.xml @@ -305,219 +305,219 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - - - - + @@ -533,27 +533,27 @@ - - - - - - - - + + + + + + + + @@ -644,2455 +644,2455 @@ + + + - + + + - - - + - - - - - - - - - - - - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + + - + + + - - + + + - - - - + + + - - - - - - - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + + - + + + - - + + + - - - - + + + - - - - - - - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + + - + + + - - + + + - - - - + + + - - - - - - - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + + - + + + - - + + + - - - - + + + - - - - - - - - - - - - - - - - + - - + - - - - + + + + + + - - + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - + + + + @@ -3106,132 +3106,132 @@ - - + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - + + + + @@ -3245,132 +3245,132 @@ - - + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - + + + + @@ -3384,1821 +3384,1819 @@ - - + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + - + + + + + + + + + + - + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - + - - - + + - - - + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - - + - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + + + + + + + - - - - + + + - - - - - + + + - - - - - - - - - - - - - + + + - - - - - + + + + - + + + + + + + + + + - + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - + - - - + + - - - + + + + + + + + + + - + + + + + + + + + + - + + - - + + + + - - - - - - - - - - - - - - - - - - + + - - - + - - - + + - - - + + + + + + + + + + - + + + + + + + + + + - + + - - + + + + - - - - - - - - - - - - - - - - - - + + - - - + - - - + + - - - + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - - + - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - + + + - - - - - - - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - - - - - - - - - + + + - - - - - - - - - - + - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - - - - - - - - - + + + - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - - - - - - - - - + + + - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + + + - + + + + + + - - - + + + + + + + - - - - - - - - - - - - - + + + - - - - - - + + + + + + + + + + + + - + + + + @@ -5212,31 +5210,33 @@ - - + + + - - - - + + + - - - - - - - - - - - - + + + + + + + + + + + - + + + + @@ -5250,31 +5250,31 @@ - - + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - + + + + @@ -5288,106 +5288,232 @@ - - + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + - - - - - - - - - - - - + + + - - - - - - + - - - - - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application/core/resources/src/commonMain/composeResources/values-en/strings.xml b/application/core/resources/src/commonMain/composeResources/values-en/strings.xml index cb4a6731a..33046d61c 100644 --- a/application/core/resources/src/commonMain/composeResources/values-en/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values-en/strings.xml @@ -124,4 +124,7 @@ Use in offline mode [Tab - accept; Esc - remove] AI Model + Title + Subtitle + Heading diff --git a/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml b/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml index ae8580687..640ef2eaa 100644 --- a/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml @@ -118,4 +118,7 @@ User offline [Tab - aceitar; Esc - remover] Modelo IA + Título + Subtítulo + Cabeçalho diff --git a/application/core/resources/src/commonMain/composeResources/values/strings.xml b/application/core/resources/src/commonMain/composeResources/values/strings.xml index cb4a6731a..33046d61c 100644 --- a/application/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values/strings.xml @@ -124,4 +124,7 @@ Use in offline mode [Tab - accept; Esc - remove] AI Model + Title + Subtitle + Heading diff --git a/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt b/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt index 7ecd5de77..42aac364e 100644 --- a/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt +++ b/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt @@ -47,6 +47,7 @@ import writeopia.application.core.resources.generated.resources.favorites import writeopia.application.core.resources.generated.resources.folder import writeopia.application.core.resources.generated.resources.font import writeopia.application.core.resources.generated.resources.general +import writeopia.application.core.resources.generated.resources.heading import writeopia.application.core.resources.generated.resources.highlight import writeopia.application.core.resources.generated.resources.home import writeopia.application.core.resources.generated.resources.image @@ -92,11 +93,13 @@ import writeopia.application.core.resources.generated.resources.sort_by_name import writeopia.application.core.resources.generated.resources.sort_by_update import writeopia.application.core.resources.generated.resources.sorting import writeopia.application.core.resources.generated.resources.start_now +import writeopia.application.core.resources.generated.resources.subtitle import writeopia.application.core.resources.generated.resources.suggestions import writeopia.application.core.resources.generated.resources.summarize import writeopia.application.core.resources.generated.resources.system_theme import writeopia.application.core.resources.generated.resources.tap_to_start import writeopia.application.core.resources.generated.resources.text +import writeopia.application.core.resources.generated.resources.title import writeopia.application.core.resources.generated.resources.type_new_password import writeopia.application.core.resources.generated.resources.url import writeopia.application.core.resources.generated.resources.version @@ -411,4 +414,13 @@ object WrStrings { @Composable fun aiModel() = stringResource(Res.string.ai_model) + + @Composable + fun title() = stringResource(Res.string.title) + + @Composable + fun subtitle() = stringResource(Res.string.subtitle) + + @Composable + fun heading() = stringResource(Res.string.heading) } diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt index 4b589e36b..57b6a86b4 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/edit/menu/SideEditorOptions.kt @@ -388,7 +388,7 @@ private fun TitleChanges( ) { TextButton( modifier = Modifier.weight(1F), - text = "Title", + text = WrStrings.title(), highlight = metadata.contains( SelectionMetadata.TITLE ), @@ -401,7 +401,7 @@ private fun TitleChanges( TextButton( modifier = Modifier.weight(1F), - text = "Subtitle", + text = WrStrings.subtitle(), highlight = metadata.contains( SelectionMetadata.SUBTITLE ), @@ -414,7 +414,7 @@ private fun TitleChanges( TextButton( modifier = Modifier.weight(1F), - text = "Heading", + text = WrStrings.heading(), highlight = metadata.contains( SelectionMetadata.HEADING ), From c9cf023fe54e03adf92932121d4fcd71538b4c4f Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Mon, 13 Oct 2025 17:10:15 +0200 Subject: [PATCH 28/39] 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 --- .../writeopia/common/utils/icons/WrIcons.kt | 4 + .../writeopia/common/utils/icons/all/link.kt | 51 ++++++ .../io/writeopia/sdk/models/span/SpanInfo.kt | 30 ++- .../writeopia/ui/components/EditionScreen.kt | 172 +++++++++++++----- .../io/writeopia/ui/drawer/TextToolbox.kt | 2 + .../writeopia/ui/drawer/content/TextDrawer.kt | 73 ++++++-- .../writeopia/ui/extensions/SpanExtension.kt | 1 + .../ui/manager/WriteopiaStateManager.kt | 9 +- .../kotlin/io/writeopia/ui/utils/Span.kt | 39 +++- .../drawer/factory/DefaultDrawersDesktop.kt | 1 + 10 files changed, 305 insertions(+), 77 deletions(-) create mode 100644 application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/link.kt diff --git a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt index 083559853..d3a339f80 100644 --- a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt +++ b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt @@ -58,6 +58,7 @@ import io.writeopia.common.utils.icons.all.Undo2 import io.writeopia.common.utils.icons.all.WandSparkles import io.writeopia.common.utils.icons.all.X import io.writeopia.common.utils.icons.all.Zap +import io.writeopia.common.utils.icons.all.Link object WrIcons { val settings: ImageVector = Bolt @@ -172,6 +173,8 @@ object WrIcons { val chart = ChartScatter + val link = Link + val allIcons: Map = mapOf( "settings" to settings, @@ -228,6 +231,7 @@ object WrIcons { "ai" to ai, "command" to command, "chart" to chart, + "link" to link, ) fun fromName(name: String): ImageVector? = allIcons[name] diff --git a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/link.kt b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/link.kt new file mode 100644 index 000000000..43d8d4567 --- /dev/null +++ b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/link.kt @@ -0,0 +1,51 @@ +package io.writeopia.common.utils.icons.all + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +internal val Link: ImageVector + get() { + if (_link != null) return _link!! + + _link = ImageVector.Builder( + name = "link", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(10f, 13f) + arcToRelative(5f, 5f, 0f, false, false, 7.54f, 0.54f) + lineToRelative(3f, -3f) + arcToRelative(5f, 5f, 0f, false, false, -7.07f, -7.07f) + lineToRelative(-1.72f, 1.71f) + } + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(14f, 11f) + arcToRelative(5f, 5f, 0f, false, false, -7.54f, -0.54f) + lineToRelative(-3f, 3f) + arcToRelative(5f, 5f, 0f, false, false, 7.07f, 7.07f) + lineToRelative(1.71f, -1.71f) + } + }.build() + + return _link!! + } + +private var _link: ImageVector? = null diff --git a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/span/SpanInfo.kt b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/span/SpanInfo.kt index 9ee271b42..6da1a7b03 100644 --- a/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/span/SpanInfo.kt +++ b/writeopia_models/src/commonMain/kotlin/io/writeopia/sdk/models/span/SpanInfo.kt @@ -4,7 +4,12 @@ import kotlin.math.abs import kotlin.math.max import kotlin.math.min -data class SpanInfo private constructor(val start: Int, val end: Int, val span: Span) { +data class SpanInfo private constructor( + val start: Int, + val end: Int, + val span: Span, + val extra: String? = null +) { operator fun plus(spanInfo: SpanInfo) = if (spanInfo.span == spanInfo.span) { @@ -29,10 +34,16 @@ data class SpanInfo private constructor(val start: Int, val end: Int, val span: /** * Serialize the object as a string: "start:end:span" */ - fun toText(): String = "$start:$end:${span.toText()}" + fun toText(): String = "$start:$end:${span.toText()}:$extra" fun size() = abs(end - start) + fun expandable() = + when (this.span) { + Span.LINK -> false + else -> true + } + fun intersection(spanInfo: SpanInfo): Intersection { val (smaller, bigger) = orderSpansBySize(this, spanInfo) @@ -80,19 +91,25 @@ data class SpanInfo private constructor(val start: Int, val end: Int, val span: fun fromString(serialized: String): SpanInfo { val parts = serialized.split(":") - require(parts.size == 3) { "Invalid serialized format" } + require(parts.size == 3 || parts.size == 4) { "Invalid serialized format" } val start = parts[0].toIntOrNull() ?: error("Invalid start value") val end = parts[1].toIntOrNull() ?: error("Invalid end value") val span = Span.textFromString(parts[2]) + val extra = if (parts.size >= 4) parts[3] else null - return SpanInfo(start, end, span) + return SpanInfo(start, end, span, extra) } - fun create(start: Int, end: Int, span: Span): SpanInfo { + fun create( + start: Int, + end: Int, + span: Span, + extra: String? = null + ): SpanInfo { val (realStart, realEnd) = if (start <= end) start to end else end to start - return SpanInfo(realStart, realEnd, span) + return SpanInfo(realStart, realEnd, span, extra) } } } @@ -112,6 +129,7 @@ enum class Span(val label: String) { HIGHLIGHT_YELLOW("HIGHLIGHT"), HIGHLIGHT_GREEN("HIGHLIGHT_GREEN"), HIGHLIGHT_RED("HIGHLIGHT_RED"), + LINK("LINK"), NONE(""); fun toText() = this.label diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt index cb11eab3c..cfd4df663 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt @@ -1,5 +1,6 @@ package io.writeopia.ui.components +import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.PaddingValues @@ -9,20 +10,31 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ContentCut import androidx.compose.material.icons.filled.DeleteOutline import androidx.compose.material.icons.outlined.FormatBold import androidx.compose.material.icons.outlined.FormatItalic import androidx.compose.material.icons.outlined.FormatUnderlined +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties import io.writeopia.sdk.models.span.Span import io.writeopia.ui.icons.WrSdkIcons import org.jetbrains.compose.ui.tooling.preview.Preview @@ -200,62 +212,87 @@ fun EditionScreenForText( modifier: Modifier = Modifier, iconSize: Dp = 36.dp, onSpanClick: (Span) -> Unit = {}, + onLinkConfirm: (String) -> Unit = {}, ) { val iconPadding = PaddingValues(vertical = 4.dp) val clipShape = MaterialTheme.shapes.medium val spaceWidth = 8.dp - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - val tint = MaterialTheme.colorScheme.onPrimary + var showLinkScreen by remember { mutableStateOf(false) } - Row( - modifier = Modifier - .horizontalScroll(rememberScrollState()), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier - .clip(clipShape) - .clickable { - onSpanClick(Span.BOLD) - } - .size(iconSize) - .padding(iconPadding), - imageVector = Icons.Outlined.FormatBold, - contentDescription = "BOLD", -// contentDescription = stringResource(R.string.delete), - tint = tint + Crossfade(showLinkScreen) { showLink -> + if (showLink) { + AddLinkScreen( + modifier = Modifier, + cancel = { showLinkScreen = false }, + onLinkConfirm = onLinkConfirm ) + } else { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + val tint = MaterialTheme.colorScheme.onPrimary - Icon( - modifier = Modifier - .clip(clipShape) - .clickable { - onSpanClick(Span.ITALIC) - } - .size(iconSize) - .padding(iconPadding), - imageVector = Icons.Outlined.FormatItalic, - contentDescription = "ITALIC", + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .clip(clipShape) + .clickable { + onSpanClick(Span.BOLD) + } + .size(iconSize) + .padding(iconPadding), + imageVector = Icons.Outlined.FormatBold, + contentDescription = "BOLD", // contentDescription = stringResource(R.string.delete), - tint = tint - ) + tint = tint, + ) - Spacer(modifier = Modifier.width(spaceWidth)) + Icon( + modifier = Modifier + .clip(clipShape) + .clickable { + onSpanClick(Span.ITALIC) + } + .size(iconSize) + .padding(iconPadding), + imageVector = Icons.Outlined.FormatItalic, + contentDescription = "ITALIC", +// contentDescription = stringResource(R.string.delete), + tint = tint + ) - Icon( - modifier = Modifier - .clip(clipShape) - .clickable { - onSpanClick(Span.UNDERLINE) - } - .size(iconSize) - .padding(iconPadding), - imageVector = Icons.Outlined.FormatUnderlined, - contentDescription = "UNDERLINE", + Spacer(modifier = Modifier.width(spaceWidth)) + + Icon( + modifier = Modifier + .clip(clipShape) + .clickable { + onSpanClick(Span.UNDERLINE) + } + .size(iconSize) + .padding(iconPadding), + imageVector = Icons.Outlined.FormatUnderlined, + contentDescription = "UNDERLINE", // contentDescription = stringResource(R.string.delete), - tint = tint - ) + tint = tint + ) + + Spacer(modifier = Modifier.width(spaceWidth)) + + Icon( + modifier = Modifier + .clip(clipShape) + .clickable { + showLinkScreen = true + } + .size(iconSize) + .padding(PaddingValues(vertical = 5.dp)), + imageVector = WrSdkIcons.linkPage, + contentDescription = "Link to page", + tint = tint + ) // Spacer(modifier = Modifier.width(spaceWidth)) // @@ -295,6 +332,53 @@ fun EditionScreenForText( // contentDescription = "Link to page", // tint = tint // ) + } + } + } + } +} + +@Composable +private fun AddLinkScreen( + modifier: Modifier = Modifier, + cancel: () -> Unit, + onLinkConfirm: (String) -> Unit = {}, +) { + var linkText by remember { mutableStateOf("") } + + Popup(properties = PopupProperties(focusable = true), onDismissRequest = cancel) { + Card(modifier = modifier.shadow(elevation = 6.dp, shape = CardDefaults.shape)) { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + BasicTextField( + modifier = Modifier.width(300.dp), + value = linkText, + onValueChange = { linkText = it }, + maxLines = 1, + singleLine = true, + textStyle = MaterialTheme.typography + .bodyMedium + .copy(color = MaterialTheme.colorScheme.onBackground), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + imageVector = WrSdkIcons.check, + contentDescription = "Confirm", + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .clickable { + onLinkConfirm(linkText) + } + .size(28.dp) + .padding(4.dp), + tint = MaterialTheme.colorScheme.onBackground + ) + } } } } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt index 44cd38fa7..405e98f61 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt @@ -21,6 +21,7 @@ import io.writeopia.ui.components.EditionScreenForText fun TextToolbox( hasSelection: Boolean, onSpanClick: (Span) -> Unit = {}, + onLinkClick: (String) -> Unit = {}, ) { Popup(offset = IntOffset(0, -40)) { AnimatedVisibility( @@ -37,6 +38,7 @@ fun TextToolbox( .padding(horizontal = 8.dp, vertical = 2.dp), iconSize = 28.dp, onSpanClick = onSpanClick, + onLinkConfirm = onLinkClick, ) } } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt index ad4fa02f7..9deaa741d 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt @@ -1,13 +1,20 @@ package io.writeopia.ui.drawer.content +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -18,6 +25,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusState @@ -27,12 +35,19 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.withLink +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import io.writeopia.sdk.models.span.Span import io.writeopia.sdk.models.story.StoryStep import io.writeopia.sdk.models.story.StoryTypes import io.writeopia.sdk.models.story.Tag @@ -117,6 +132,17 @@ class TextDrawer( val hasSelection = selection.start != selection.end + val selectedLink by remember { + derivedStateOf { + spans + .filter { spanInfo -> spanInfo.span == Span.LINK } + .firstOrNull { (start, end, _, _) -> + selection.start >= start && selection.start <= end + } + ?.extra + } + } + val realPosition by remember { derivedStateOf { val lineStart = textLayoutResult?.multiParagraph?.getLineStart(cursorLine ?: 0) @@ -242,20 +268,39 @@ class TextDrawer( } textToolbox(hasSelection) -// Popup(offset = IntOffset(0, -70)) { -// AnimatedVisibility( -// visible = hasSelection, -// enter = fadeIn(animationSpec = tween(durationMillis = 150)), -// exit = fadeOut(animationSpec = tween(durationMillis = 150)) -// ) { -// EditionScreen( -// modifier = Modifier -// .padding(bottom = 20.dp) -// .clip(MaterialTheme.shapes.large) -// .background(MaterialTheme.colorScheme.primary) -// ) -// } -// } + + Popup(offset = IntOffset(0, -20)) { + AnimatedVisibility( + visible = selectedLink != null, + enter = fadeIn(animationSpec = tween(durationMillis = 150)), + exit = fadeOut(animationSpec = tween(durationMillis = 150)) + ) { + LinkHandler(selectedLink ?: "") + } + } + } + } +} + +@Composable +private fun LinkHandler(link: String, modifier: Modifier = Modifier) { + Card(modifier = modifier) { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val text = buildAnnotatedString { + val fixedLink = link.takeIf { it.startsWith("http") } ?: "https://$link" + + withLink(link = LinkAnnotation.Url(url = fixedLink)) { + append(link) + } + } + + BasicText( + text, + style = TextStyle.Default.copy(color = MaterialTheme.colorScheme.onBackground), + ) } } } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/extensions/SpanExtension.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/extensions/SpanExtension.kt index c571d62b4..b663efb9c 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/extensions/SpanExtension.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/extensions/SpanExtension.kt @@ -31,4 +31,5 @@ fun Span.toSpanStyle(isDarkTheme: Boolean): SpanStyle = } Span.NONE -> SpanStyle() + Span.LINK -> SpanStyle() } diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt index 768680acd..1ce1200c2 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt @@ -906,7 +906,7 @@ class WriteopiaStateManager( _currentStory.value = currentStory.value.copy(lastEdit = LastEdit.Metadata) } - fun toggleSpan(span: Span) { + fun toggleSpan(span: Span, extra: String? = null) { if (isEditable) { val onEdit = _positionsOnEdit.value @@ -915,18 +915,21 @@ class WriteopiaStateManager( writeopiaManager.addSpanToStories(_currentStory.value, onEdit, span) } else { val selection = currentStory.value.selection - val (start, end) = selection.sortedPositions() _currentStory.value = writeopiaManager.addSpan( _currentStory.value, selection.position, - SpanInfo.create(start, end, span) + SpanInfo.create(start, end, span, extra) ) } } } + fun onLinkSet(link: String) { + toggleSpan(Span.LINK, link) + } + fun addImage(imagePath: String, position: Int? = null) { if (!isEditable) return (position ?: currentPosition())?.let { pos -> diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt index 312e9390f..fbdeb3347 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt @@ -1,10 +1,13 @@ package io.writeopia.ui.utils +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import io.writeopia.sdk.models.span.Span import io.writeopia.sdk.models.span.SpanInfo import io.writeopia.ui.extensions.toSpanStyle -import kotlin.math.abs import kotlin.math.min object Spans { @@ -18,20 +21,36 @@ object Spans { return buildAnnotatedString { append(text.takeIf { it?.isNotEmpty() == true } ?: "") - spans.forEach { spanInfo -> - addStyle( - spanInfo.span.toSpanStyle(isDarkTheme), - min(lastPosition, spanInfo.start), - min(lastPosition, spanInfo.end) - ) - } + spans.filter { spanInfo -> spanInfo.span != Span.LINK } + .forEach { spanInfo -> + addStyle( + spanInfo.span.toSpanStyle(isDarkTheme), + min(lastPosition, spanInfo.start), + min(lastPosition, spanInfo.end) + ) + } + + spans.filter { spanInfo -> spanInfo.span == Span.LINK } + .forEach { spanInfo -> + addStyle( + SpanStyle( + color = Color.Blue, + textDecoration = TextDecoration.Underline + ), + min(lastPosition, spanInfo.start), + min(lastPosition, spanInfo.end) + ) + } } } fun recalculateSpans(spans: Set, position: Int, change: Int): Set { - val toChangeSize = spans.filterTo(mutableSetOf()) { span -> span.isInside(position) } + val toChangeSize = spans + .filterTo(mutableSetOf()) { span -> + span.isInside(position) + } val sizeChanged = toChangeSize.mapTo(mutableSetOf()) { span -> - if (abs(change) > 0) { + if (change > 0 && span.expandable() || change < 0) { span.changeSize(change) } else { span diff --git a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt index bec702b9b..f5fed23a1 100644 --- a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt +++ b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt @@ -51,6 +51,7 @@ object DefaultDrawersDesktop : DrawersFactory { TextToolbox( hasSelection = hasSelection, onSpanClick = manager::toggleSpan, + onLinkClick = manager::onLinkSet ) } From fc6a5679d28daee265097473962d10c7f3889a19 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Mon, 13 Oct 2025 17:33:35 +0200 Subject: [PATCH 29/39] Changing link color --- .../kotlin/io/writeopia/ui/utils/Span.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt index fbdeb3347..b01411a40 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/utils/Span.kt @@ -32,11 +32,20 @@ object Spans { spans.filter { spanInfo -> spanInfo.span == Span.LINK } .forEach { spanInfo -> - addStyle( + val style = if (isDarkTheme) { + SpanStyle( + color = Color(0xFF9E9E9E), + textDecoration = TextDecoration.Underline + ) + } else { SpanStyle( - color = Color.Blue, + color = Color(0xFF9E9E9E), textDecoration = TextDecoration.Underline - ), + ) + } + + addStyle( + style, min(lastPosition, spanInfo.start), min(lastPosition, spanInfo.end) ) From b425bfed2cf1fc4bdd156ca70fcd37c9fac7deb3 Mon Sep 17 00:00:00 2001 From: Guilherme Miranda Date: Sun, 19 Oct 2025 21:44:57 +0200 Subject: [PATCH 30/39] Enhance search in document feature (#512) * Add new icon and text * Highlight the current result based on last position and control search index * Add results count and chevrons to iterate from results --- .../composeResources/values-en/strings.xml | 1 + .../composeResources/values-pt/strings.xml | 1 + .../composeResources/values/strings.xml | 1 + .../io/writeopia/resources/WrStrings.kt | 4 ++ .../writeopia/common/utils/icons/WrIcons.kt | 3 + .../common/utils/icons/all/ChevronUp.kt | 46 +++++++++++++ .../ui/desktop/DesktopNoteEditorScreen.kt | 66 +++++++++++++++---- .../viewmodel/NoteEditorKmpViewModel.kt | 55 ++++++++++++++-- .../editor/viewmodel/NoteEditorViewModel.kt | 8 +++ 9 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/ChevronUp.kt diff --git a/application/core/resources/src/commonMain/composeResources/values-en/strings.xml b/application/core/resources/src/commonMain/composeResources/values-en/strings.xml index 33046d61c..0356aeb24 100644 --- a/application/core/resources/src/commonMain/composeResources/values-en/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values-en/strings.xml @@ -37,6 +37,7 @@ Enter without register Register Search + 0 results Home Favorites Settings diff --git a/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml b/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml index 640ef2eaa..7e020d706 100644 --- a/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml @@ -37,6 +37,7 @@ Entrar without register Registrar Pesquisar + 0 resultados Home Favoritos Configurações diff --git a/application/core/resources/src/commonMain/composeResources/values/strings.xml b/application/core/resources/src/commonMain/composeResources/values/strings.xml index 33046d61c..0356aeb24 100644 --- a/application/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values/strings.xml @@ -37,6 +37,7 @@ Enter without register Register Search + 0 results Home Favorites Settings diff --git a/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt b/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt index 42aac364e..797d11756 100644 --- a/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt +++ b/application/core/resources/src/commonMain/kotlin/io/writeopia/resources/WrStrings.kt @@ -84,6 +84,7 @@ import writeopia.application.core.resources.generated.resources.recent import writeopia.application.core.resources.generated.resources.repeat_password import writeopia.application.core.resources.generated.resources.retry import writeopia.application.core.resources.generated.resources.search +import writeopia.application.core.resources.generated.resources.search_no_results import writeopia.application.core.resources.generated.resources.settings import writeopia.application.core.resources.generated.resources.sign_in import writeopia.application.core.resources.generated.resources.sign_in_account @@ -112,6 +113,9 @@ object WrStrings { @Composable fun search() = stringResource(Res.string.search) + @Composable + fun searchNoResults() = stringResource(Res.string.search_no_results) + @Composable fun home() = stringResource(Res.string.home) diff --git a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt index d3a339f80..5f426bc76 100644 --- a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt +++ b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/WrIcons.kt @@ -20,6 +20,7 @@ import io.writeopia.common.utils.icons.all.CalendarArrowDown import io.writeopia.common.utils.icons.all.ChartScatter import io.writeopia.common.utils.icons.all.ChevronDown import io.writeopia.common.utils.icons.all.ChevronRight +import io.writeopia.common.utils.icons.all.ChevronUp import io.writeopia.common.utils.icons.all.CircleArrowLeft import io.writeopia.common.utils.icons.all.CircleArrowRight import io.writeopia.common.utils.icons.all.CirclePlus @@ -131,6 +132,8 @@ object WrIcons { val smallArrowDown: ImageVector = ChevronDown + val smallArrowUp: ImageVector = ChevronUp + val undo: ImageVector = Undo2 val redo: ImageVector = Redo2 diff --git a/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/ChevronUp.kt b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/ChevronUp.kt new file mode 100644 index 000000000..5c792fd5c --- /dev/null +++ b/application/core/utils/src/commonMain/kotlin/io/writeopia/common/utils/icons/all/ChevronUp.kt @@ -0,0 +1,46 @@ +package io.writeopia.common.utils.icons.all +/* +* Converted using https://composables.com/svgtocompose +*/ + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +internal val ChevronUp: ImageVector + get() { + if (_ChevronUp != null) { + return _ChevronUp!! + } + _ChevronUp = ImageVector.Builder( + name = "io.writeopia.common.utils.icons.all.getChevronUp", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + fill = null, + fillAlpha = 1.0f, + stroke = SolidColor(Color(0xFF000000)), + strokeAlpha = 1.0f, + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(18f, 15f) + lineToRelative(-6f, -6f) + lineToRelative(-6f, 6f) + } + }.build() + return _ChevronUp!! + } + +private var _ChevronUp: ImageVector? = null diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt index 364c9496a..9eae8539f 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons @@ -21,6 +22,7 @@ import androidx.compose.material.icons.outlined.Lock import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -34,6 +36,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.writeopia.common.utils.icons.WrIcons import io.writeopia.commonui.dialogs.confirmation.DeleteConfirmationDialog @@ -41,6 +44,7 @@ import io.writeopia.editor.features.editor.ui.desktop.edit.menu.SideEditorOption import io.writeopia.editor.features.editor.ui.folders.FolderSelectionDialog import io.writeopia.editor.features.editor.viewmodel.NoteEditorViewModel import io.writeopia.editor.features.editor.viewmodel.SideMenuTab +import io.writeopia.resources.WrStrings import io.writeopia.theme.WriteopiaTheme import io.writeopia.ui.drawer.factory.DrawersFactory @@ -100,6 +104,8 @@ fun DesktopNoteEditorScreen( val textState by noteEditorViewModel.searchText.collectAsState() val showSearch by noteEditorViewModel.showSearchState.collectAsState() + val currentResultIndex by noteEditorViewModel.currentSearchIndexState.collectAsState() + val totalResults by noteEditorViewModel.totalSearchResultsState.collectAsState() val shape = MaterialTheme.shapes.medium AnimatedVisibility( @@ -110,21 +116,55 @@ fun DesktopNoteEditorScreen( Box(modifier = Modifier.padding(6.dp)) { val focusRequester = remember { FocusRequester() } - BasicTextField( - value = textState, - onValueChange = noteEditorViewModel::searchInDocument, - modifier = Modifier.defaultMinSize(minWidth = 160.dp) - .focusRequester(focusRequester) + Row( + modifier = Modifier .padding(12.dp) .background(WriteopiaTheme.colorScheme.cardBg, shape) - .border(1.dp, MaterialTheme.colorScheme.outline, shape) - .padding(8.dp), - singleLine = true, - textStyle = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onBackground - ), - cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), - ) + .border(1.dp, MaterialTheme.colorScheme.outline, shape), + verticalAlignment = Alignment.CenterVertically + ) { + BasicTextField( + value = textState, + onValueChange = noteEditorViewModel::searchInDocument, + modifier = Modifier.defaultMinSize(minWidth = 160.dp) + .focusRequester(focusRequester) + .padding(8.dp), + singleLine = true, + textStyle = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), + ) + + Text( + modifier = Modifier.padding(4.dp).width(100.dp), + text = if (totalResults == 0) WrStrings.searchNoResults() else "${currentResultIndex + 1}/$totalResults", + textAlign = TextAlign.Right, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground + ) + ) + + Icon( + imageVector = WrIcons.smallArrowUp, + contentDescription = "Previous result", + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .size(32.dp) + .clickable(onClick = noteEditorViewModel::previousSearchResult) + .padding(4.dp) + ) + + Icon( + imageVector = WrIcons.smallArrowDown, + contentDescription = "Next result", + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .size(32.dp) + .clickable(onClick = noteEditorViewModel::nextSearchResult) + .padding(4.dp) + ) + } Icon( imageVector = WrIcons.close, diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt index 53d49f8b7..afc94dad7 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt @@ -58,6 +58,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -142,9 +143,13 @@ class NoteEditorKmpViewModel( private val _showSearch = MutableStateFlow(false) private val _searchText = MutableStateFlow("") + private val _currentSearchIndex = MutableStateFlow(0) + private val _totalSearchResults = MutableStateFlow(0) override val showSearchState: StateFlow = _showSearch.asStateFlow() override val searchText: StateFlow = _searchText.asStateFlow() + override val currentSearchIndexState: StateFlow = _currentSearchIndex.asStateFlow() + override val totalSearchResultsState: StateFlow = _totalSearchResults.asStateFlow() private val hasLinesSelection = writeopiaManager.onEditPositions .map { it.isNotEmpty() } @@ -272,12 +277,15 @@ class NoteEditorKmpViewModel( writeopiaManager.toDraw, findsOfSearch, searchText, - _showSearch - ) { drawState, finds, query, showSearch -> + _showSearch, + _currentSearchIndex + ) { drawState, finds, query, showSearch, currentSearchIndex -> if (finds.isEmpty() && showSearch) return@combine drawState.copy(focus = null) if (finds.isEmpty()) return@combine drawState val mutableStories = drawState.stories.toMutableList() + val activeFindPosition = if (finds.size > currentSearchIndex) finds.elementAt(currentSearchIndex) else null + _totalSearchResults.value = finds.size finds.forEach { position -> val realPosition = minOf(position * 2, mutableStories.lastIndex) @@ -286,7 +294,12 @@ class NoteEditorKmpViewModel( val findSpans = FindInText.findInText(story.text ?: "", query) .map { (start, end) -> - SpanInfo.create(start, end, Span.HIGHLIGHT_YELLOW) + val span = if (position == activeFindPosition) { + Span.HIGHLIGHT_GREEN + } else { + Span.HIGHLIGHT_YELLOW + } + SpanInfo.create(start, end, span) } mutableStories[realPosition] = @@ -718,11 +731,45 @@ class NoteEditorKmpViewModel( _showSearch.value = false delay(100) _searchText.value = "" + _currentSearchIndex.value = 0 } } override fun searchInDocument(query: String) { - _searchText.value = query + viewModelScope.launch { + _searchText.value = query + val finds = findsOfSearch.first().toList().sorted() + if (finds.isEmpty()) { + _currentSearchIndex.value = 0 + _totalSearchResults.value = 0 + return@launch + } + + val currentPosition = writeopiaManager.currentStory.value.focus ?: writeopiaManager.currentStory.value.selection.position + val newIndex = finds.indexOfFirst { it >= currentPosition } + + _currentSearchIndex.value = if (newIndex != -1) newIndex else 0 + } + } + + override fun previousSearchResult() { + viewModelScope.launch { + val total = findsOfSearch.first().size + if (total == 0) return@launch + + val currentIndex = _currentSearchIndex.value + _currentSearchIndex.value = (currentIndex - 1 + total) % total + } + } + + override fun nextSearchResult() { + viewModelScope.launch { + val total = findsOfSearch.first().size + if (total == 0) return@launch + + val currentIndex = _currentSearchIndex.value + _currentSearchIndex.value = (currentIndex + 1) % total + } } override fun titleClick(tag: Tag) { diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt index d71c71f04..abf302987 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt @@ -52,6 +52,10 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { val searchText: StateFlow + val currentSearchIndexState: StateFlow + + val totalSearchResultsState: StateFlow + val hasSelectedLines: StateFlow val selectionMetadataState: StateFlow> @@ -66,6 +70,10 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler { fun searchInDocument(query: String) + fun previousSearchResult() + + fun nextSearchResult() + fun toggleEditable() fun deleteSelection() From e7c45e48d7c842176558a302fed36c4fa252f8c4 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Fri, 24 Oct 2025 19:06:00 +0200 Subject: [PATCH 31/39] Fixing performance of editor (#513) * Fixing performance of editor * Update baseline.xml --------- Co-authored-by: Leandro Ferreira --- .../core/resources/config/ktlint/baseline.xml | 7423 +++++++++-------- .../io/writeopia/ui/drawer/TextToolbox.kt | 13 +- .../writeopia/ui/drawer/content/TextDrawer.kt | 12 +- 3 files changed, 3738 insertions(+), 3710 deletions(-) diff --git a/application/core/resources/config/ktlint/baseline.xml b/application/core/resources/config/ktlint/baseline.xml index bdee5362d..5f6dac990 100644 --- a/application/core/resources/config/ktlint/baseline.xml +++ b/application/core/resources/config/ktlint/baseline.xml @@ -437,90 +437,90 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + + + + @@ -542,11 +542,7 @@ - - - - @@ -556,7 +552,11 @@ + + + + @@ -656,3856 +656,3848 @@ + + + - - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + + + - - - - - - - - - + + - - - - - - + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + - - - - - - - - - - + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - @@ -4519,31 +4511,33 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + - - - - + @@ -4557,31 +4551,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -4595,72 +4589,70 @@ - - - + + + + + + - - - + + + + + + + + + - - - + + + - - - - - - - + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + - - - - - - - - - - - - + + + + + + + + + + - - - @@ -4674,72 +4666,72 @@ - - - + + + + + + - - - + + + + + + + + + - - - + + + + + - - - - - - - + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + - - - - - - - - - - - - + + + + + + + + + + - - - @@ -4753,31 +4745,33 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + - - - - + @@ -4791,31 +4785,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -4829,31 +4823,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -4867,31 +4861,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -4905,31 +4899,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -4943,31 +4937,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -4981,31 +4975,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5019,31 +5013,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5057,31 +5051,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5095,31 +5089,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5133,31 +5127,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5171,72 +5165,70 @@ - - - + + + + + + - - - + + + + + + + + + - - - + + + - - - - - - - + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + - - - - - - - - - - - - + + + + + + + + + + - - - @@ -5250,31 +5242,33 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + - - - - + @@ -5288,94 +5282,94 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - - - - + + + + + + + - - - + + + + + + + + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - + + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5389,31 +5383,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5427,31 +5421,31 @@ - - - + + + + + + - - - + + + + + + + + + - - - - - - - - - - - + + + + + - - - - + @@ -5465,55 +5459,106 @@ - - - + + + + + + - - - + + + + + + + + + - - - + + + - - - - - - - + - - - - - - - + + + + + + + + - - - - - - - - + + + + + - - + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt index 405e98f61..936d89d14 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/TextToolbox.kt @@ -1,9 +1,5 @@ package io.writeopia.ui.drawer -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -23,13 +19,8 @@ fun TextToolbox( onSpanClick: (Span) -> Unit = {}, onLinkClick: (String) -> Unit = {}, ) { - Popup(offset = IntOffset(0, -40)) { - AnimatedVisibility( - modifier = Modifier, - visible = hasSelection, - enter = fadeIn(animationSpec = tween(durationMillis = 150)), - exit = fadeOut(animationSpec = tween(durationMillis = 150)) - ) { + if (hasSelection) { + Popup(offset = IntOffset(0, -40)) { EditionScreenForText( modifier = Modifier .shadow(elevation = 6.dp) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt index 9deaa741d..46121dc3f 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt @@ -1,9 +1,5 @@ package io.writeopia.ui.drawer.content -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -269,12 +265,8 @@ class TextDrawer( textToolbox(hasSelection) - Popup(offset = IntOffset(0, -20)) { - AnimatedVisibility( - visible = selectedLink != null, - enter = fadeIn(animationSpec = tween(durationMillis = 150)), - exit = fadeOut(animationSpec = tween(durationMillis = 150)) - ) { + if (selectedLink != null) { + Popup(offset = IntOffset(0, -20)) { LinkHandler(selectedLink ?: "") } } From 0ee02f15d61bc1993a359e72059d67c67419d729 Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Wed, 29 Oct 2025 22:17:03 +0100 Subject: [PATCH 32/39] Scrolling to active search result (#514) * Scrolling to active search result * increasing bottom margin --------- Co-authored-by: Leandro Ferreira --- .../editor/features/editor/ui/TextEditor.kt | 8 ++++++-- .../ui/desktop/DesktopNoteEditorScreen.kt | 17 ++++++++++++++++- .../editor/viewmodel/NoteEditorKmpViewModel.kt | 10 ++++++++-- .../ui/manager/WriteopiaStateManager.kt | 6 +++++- .../ui/drawer/factory/DefaultDrawersDesktop.kt | 2 +- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt index 2a96483cf..8ca7571b0 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/TextEditor.kt @@ -38,8 +38,12 @@ internal fun TextEditor( if (position != null) { LaunchedEffect(position, block = { - noteEditorViewModel.scrollToPosition.collectLatest { - listState.animateScrollBy(70F) + noteEditorViewModel.scrollToPosition.collectLatest { position -> + if (position == -1) { + listState.animateScrollBy(70F) + } else if (position != null) { + listState.scrollToItem(position, scrollOffset = -100) + } } }) } diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt index 9eae8539f..1b4d026f3 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt @@ -36,6 +36,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.writeopia.common.utils.icons.WrIcons @@ -128,7 +133,17 @@ fun DesktopNoteEditorScreen( onValueChange = noteEditorViewModel::searchInDocument, modifier = Modifier.defaultMinSize(minWidth = 160.dp) .focusRequester(focusRequester) - .padding(8.dp), + .padding(8.dp) + .onPreviewKeyEvent { keyEvent -> + if (keyEvent.type == KeyEventType.KeyUp && + keyEvent.key.keyCode == Key.Enter.keyCode + ) { + noteEditorViewModel.nextSearchResult() + true + } + + false + }, singleLine = true, textStyle = MaterialTheme.typography.bodySmall.copy( color = MaterialTheme.colorScheme.onBackground diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt index afc94dad7..7b4dca25b 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt @@ -284,9 +284,14 @@ class NoteEditorKmpViewModel( if (finds.isEmpty()) return@combine drawState val mutableStories = drawState.stories.toMutableList() - val activeFindPosition = if (finds.size > currentSearchIndex) finds.elementAt(currentSearchIndex) else null + val activeFindPosition = + if (finds.size > currentSearchIndex) finds.elementAt(currentSearchIndex) else null _totalSearchResults.value = finds.size + if (activeFindPosition != null) { + writeopiaManager.scrollToPosition(activeFindPosition) + } + finds.forEach { position -> val realPosition = minOf(position * 2, mutableStories.lastIndex) val toDraw = mutableStories[realPosition] @@ -745,7 +750,8 @@ class NoteEditorKmpViewModel( return@launch } - val currentPosition = writeopiaManager.currentStory.value.focus ?: writeopiaManager.currentStory.value.selection.position + val currentPosition = writeopiaManager.currentStory.value.focus + ?: writeopiaManager.currentStory.value.selection.position val newIndex = finds.indexOfFirst { it >= currentPosition } _currentSearchIndex.value = if (newIndex != -1) newIndex else 0 diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt index 1ce1200c2..9132fa554 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt @@ -648,7 +648,7 @@ class WriteopiaStateManager( // Todo: Fix this when the inner position are completed // backStackManager.addAction(BackstackAction.Add(newStory, newPosition)) _currentStory.value = newState.copy(selection = Selection.start()) - _scrollToPosition.value = newPosition + _scrollToPosition.value = -1 } } } @@ -660,6 +660,10 @@ class WriteopiaStateManager( _currentStory.value = story.copy(focus = position) } + fun scrollToPosition(position: Int) { + _scrollToPosition.value = position + } + private fun selected(isSelected: Boolean, position: Int) { if (!isEditable) return if (_currentStory.value.stories[position] != null) { diff --git a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt index f5fed23a1..b695bf95c 100644 --- a/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt +++ b/writeopia_ui/src/jvmMain/kotlin/io/writeopia/ui/drawer/factory/DefaultDrawersDesktop.kt @@ -57,7 +57,7 @@ object DefaultDrawersDesktop : DrawersFactory { return CommonDrawers.create( manager, - 30.dp, + 300.dp, aiExplanation, editable, onHeaderClick, From d7e45fd564f3414a5dfe36afba96ce087312be2e Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Thu, 30 Oct 2025 14:05:43 +0100 Subject: [PATCH 33/39] Adding enter of numpad in the trigger of search --- .../features/editor/ui/desktop/DesktopNoteEditorScreen.kt | 4 ++-- .../io/writeopia/ui/drawer/factory/KeyEventListenerFactory.kt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt index 1b4d026f3..c6fdb86ef 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/DesktopNoteEditorScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent @@ -52,6 +51,7 @@ import io.writeopia.editor.features.editor.viewmodel.SideMenuTab import io.writeopia.resources.WrStrings import io.writeopia.theme.WriteopiaTheme import io.writeopia.ui.drawer.factory.DrawersFactory +import io.writeopia.ui.drawer.factory.isEnterKey @Composable fun DesktopNoteEditorScreen( @@ -136,7 +136,7 @@ fun DesktopNoteEditorScreen( .padding(8.dp) .onPreviewKeyEvent { keyEvent -> if (keyEvent.type == KeyEventType.KeyUp && - keyEvent.key.keyCode == Key.Enter.keyCode + keyEvent.key.isEnterKey() ) { noteEditorViewModel.nextSearchResult() true diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/KeyEventListenerFactory.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/KeyEventListenerFactory.kt index 8eaa284dc..815f66424 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/KeyEventListenerFactory.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/KeyEventListenerFactory.kt @@ -159,3 +159,5 @@ private fun isMoveStrongDownEvent(keyEvent: KeyEvent) = private fun isLineBreak(keyEvent: KeyEvent): Boolean = keyEvent.key == Key.Enter +fun Key.isEnterKey(): Boolean = + this.keyCode == Key.Enter.keyCode || this.keyCode == Key.NumPadEnter.keyCode From 1327eaa83ab577b5f986fc523d5fbb0945d3afd0 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 31 Oct 2025 01:30:15 +0100 Subject: [PATCH 34/39] showing only icons when side menu is narrow --- .../writeopia/global/shell/SideGlobalMenu.kt | 137 +++++++++++------- 1 file changed, 84 insertions(+), 53 deletions(-) diff --git a/application/features/global_shell/src/commonMain/kotlin/io/writeopia/global/shell/SideGlobalMenu.kt b/application/features/global_shell/src/commonMain/kotlin/io/writeopia/global/shell/SideGlobalMenu.kt index 46cb17cd7..dd0e0363e 100644 --- a/application/features/global_shell/src/commonMain/kotlin/io/writeopia/global/shell/SideGlobalMenu.kt +++ b/application/features/global_shell/src/commonMain/kotlin/io/writeopia/global/shell/SideGlobalMenu.kt @@ -69,7 +69,11 @@ fun SideGlobalMenu( val widthAnimatedState by animateDpAsState(widthState) val showContent by derivedStateOf { - widthState > width * 0.3F + when { + widthState > 80.dp -> ShowContent.FULL + widthState > 40.dp -> ShowContent.ICONS + else -> ShowContent.HIDE + } } Row( @@ -77,10 +81,16 @@ fun SideGlobalMenu( verticalAlignment = Alignment.CenterVertically ) { Box(modifier = Modifier.width(widthAnimatedState).fillMaxHeight()) { - if (showContent) { + if (showContent != ShowContent.HIDE) { val menuItems by foldersState.collectAsState() - Column { + Column( + horizontalAlignment = if (showContent == ShowContent.ICONS) { + Alignment.CenterHorizontally + } else { + Alignment.Start + } + ) { Spacer( modifier = Modifier.height(48.dp).fillMaxWidth() .combinedClickable( @@ -91,68 +101,74 @@ fun SideGlobalMenu( ) ) - settingsOptions( + SettingsOptions( + showContent = showContent, iconVector = WrIcons.search, contentDescription = WrStrings.search(), text = WrStrings.search(), click = searchClick ) - settingsOptions( + SettingsOptions( + showContent = showContent, iconVector = WrIcons.home, contentDescription = WrStrings.home(), text = WrStrings.home(), click = homeClick ) - settingsOptions( + SettingsOptions( + showContent = showContent, iconVector = WrIcons.favorites, contentDescription = WrStrings.favorites(), text = WrStrings.favorites(), click = favoritesClick ) - settingsOptions( + SettingsOptions( + showContent = showContent, iconVector = WrIcons.settings, contentDescription = WrStrings.settings(), text = WrStrings.settings(), click = settingsClick, ) - title( - text = WrStrings.folder(), - trailingContent = { - Icon( - imageVector = WrIcons.target, - contentDescription = "Select opened file", - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.size(30.dp) - .clip(RoundedCornerShape(8.dp)) - .clickable(onClick = highlightContent) - .padding(6.dp) - ) + if (showContent == ShowContent.FULL) { + title( + text = WrStrings.folder(), + trailingContent = { + Icon( + imageVector = WrIcons.target, + contentDescription = "Select opened file", + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(30.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = highlightContent) + .padding(6.dp) + ) - Icon( - imageVector = WrIcons.addCircle, - contentDescription = "Add Folder", - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.size(30.dp) - .clip(RoundedCornerShape(8.dp)) - .clickable(onClick = addFolder) - .padding(6.dp) - ) - } - ) + Icon( + imageVector = WrIcons.addCircle, + contentDescription = "Add Folder", + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(30.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = addFolder) + .padding(6.dp) + ) + } + ) - DocumentList( - menuItems = menuItems, - editFolder = editFolder, - selectedFolder = navigateToFolder, - selectedDocument = navigateToEditDocument, - moveRequest = moveRequest, - expandFolder = expandFolder, - changeIcon = changeIcon - ) + DocumentList( + menuItems = menuItems, + editFolder = editFolder, + selectedFolder = navigateToFolder, + selectedDocument = navigateToEditDocument, + moveRequest = moveRequest, + expandFolder = expandFolder, + changeIcon = changeIcon + ) + } } } } @@ -160,7 +176,8 @@ fun SideGlobalMenu( } @Composable -private fun settingsOptions( +private fun SettingsOptions( + showContent: ShowContent, iconVector: ImageVector?, contentDescription: String, text: String, @@ -181,7 +198,13 @@ private fun settingsOptions( } } .padding(start = 14.dp, end = 10.dp, top = 10.dp, bottom = 10.dp) - .fillMaxWidth() + .let { modifierLet -> + if (showContent == ShowContent.FULL) { + modifierLet.fillMaxWidth() + } else { + modifierLet + } + } ) { iconVector?.let { icon -> Icon( @@ -192,21 +215,23 @@ private fun settingsOptions( ) } - Spacer(modifier = Modifier.width(10.dp)) + if (showContent == ShowContent.FULL) { + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = text, - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodySmall.copy( - fontWeight = FontWeight.Bold - ), - maxLines = 1 - ) + Text( + text = text, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Bold + ), + maxLines = 1 + ) - if (trailingContent != null) { - Spacer(modifier = Modifier.weight(1F)) + if (trailingContent != null) { + Spacer(modifier = Modifier.weight(1F)) - trailingContent() + trailingContent() + } } } } @@ -267,3 +292,9 @@ fun SideGlobalMenuPreview() { toggleMaxScreen = {} ) } + +private enum class ShowContent { + HIDE, + ICONS, + FULL +} From c5aafa41178484717a190493d354af174e12000f Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 31 Oct 2025 12:11:35 +0100 Subject: [PATCH 35/39] Reducing debouncing of search --- .../io/writeopia/features/search/ui/SearchKmpViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt b/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt index 57b04d7d2..e2bb5aa2a 100644 --- a/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt +++ b/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt @@ -23,7 +23,7 @@ class SearchKmpViewModel( @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) override val queryResults by lazy { searchState - .debounce(2000) + .debounce(500) .flatMapLatest { query -> searchRepository.searchNotesAndFolders(query) }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) From eb37939d1ae6d5f464c75a0248455e25122f98fe Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 31 Oct 2025 13:40:23 +0100 Subject: [PATCH 36/39] Improving search --- .../search/repository/SearchRepository.kt | 44 ++++++++++++++++--- .../features/search/ui/SearchKmpViewModel.kt | 13 ++++-- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/repository/SearchRepository.kt b/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/repository/SearchRepository.kt index ac66b5354..4be5d3f1f 100644 --- a/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/repository/SearchRepository.kt +++ b/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/repository/SearchRepository.kt @@ -18,12 +18,41 @@ class SearchRepository( private val searchApi: SearchApi, private val authRepository: AuthRepository, ) { - fun searchNotesAndFolders(query: String): Flow> { + fun searchNotesAndFoldersLocally(query: String): Flow> { if (query.isEmpty()) return flow { emit(getNotesAndFolders()) } - val foldersFlow: Flow> = flow { - emit(folderSearch.search(query)) + val foldersFlow: Flow> = flow { emit(folderSearch.search(query)) } + + val documentsFlow: Flow> = flow { + emit( + documentSearch.search( + query, + authRepository.getUser().id, + // Todo: Add company later + null + ) + ) } + + return combine(foldersFlow, documentsFlow) { folders, documents -> + (folders + documents).toSearchItems() + } + } + + fun searchNotesAndFoldersRemotely(query: String): Flow> { + return flow { +// println("triggering documentsApiFlow") +// emit(emptyList()) +// println("calling api") + emit(searchApi.searchApi(query).toSearchItems()) + } + } + + fun searchNotesAndFolders(query: String): Flow> { + if (query.isEmpty()) return flow { emit(getNotesAndFolders()) } + + val foldersFlow: Flow> = flow { emit(folderSearch.search(query)) } + val documentsFlow: Flow> = flow { emit( documentSearch.search( @@ -34,10 +63,11 @@ class SearchRepository( ) ) } + val documentsApiFlow: Flow> = flow { - println("triggering documentsApiFlow") - emit(emptyList()) - println("calling api") +// println("triggering documentsApiFlow") +// emit(emptyList()) +// println("calling api") emit(searchApi.searchApi(query)) } @@ -60,7 +90,7 @@ class SearchRepository( } } -fun List.toSearchItems(): List = +private fun List.toSearchItems(): List = this.map { menuItem -> when (menuItem) { is Folder -> SearchItem.FolderInfo(menuItem.id, menuItem.title) diff --git a/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt b/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt index e2bb5aa2a..29809eaa7 100644 --- a/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt +++ b/application/features/search/src/commonMain/kotlin/io/writeopia/features/search/ui/SearchKmpViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.stateIn @@ -22,11 +23,15 @@ class SearchKmpViewModel( @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) override val queryResults by lazy { - searchState + val localFlow = + searchState.flatMapLatest(searchRepository::searchNotesAndFoldersLocally) + + val remoteFlow = searchState .debounce(500) - .flatMapLatest { query -> - searchRepository.searchNotesAndFolders(query) - }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + .flatMapLatest(searchRepository::searchNotesAndFoldersRemotely) + + combine(localFlow, remoteFlow) { local, remote -> local + remote } + .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) } override fun init() {} From 83bf54c7b5e2e1baec2c2a0289556093a58ebc18 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 31 Oct 2025 14:30:21 +0100 Subject: [PATCH 37/39] adding migration for FTS --- .../src/commonMain/sqldelight/migration/6.sqm | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 application/core/persistence_sqldelight/src/commonMain/sqldelight/migration/6.sqm diff --git a/application/core/persistence_sqldelight/src/commonMain/sqldelight/migration/6.sqm b/application/core/persistence_sqldelight/src/commonMain/sqldelight/migration/6.sqm new file mode 100644 index 000000000..b99f6fc3a --- /dev/null +++ b/application/core/persistence_sqldelight/src/commonMain/sqldelight/migration/6.sqm @@ -0,0 +1,6 @@ +CREATE VIRTUAL TABLE storyStepEntityFts USING fts4( + text TEXT, + id TEXT, + document_id TEXT UNINDEXED, + position INTEGER UNINDEXED +); From f67ecb3828d1ad2c45e82e60dd9210421758bd7a Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 31 Oct 2025 15:04:41 +0100 Subject: [PATCH 38/39] Update proguard-rules-desktop.pro --- application/composeApp/proguard-rules-desktop.pro | 1 + 1 file changed, 1 insertion(+) diff --git a/application/composeApp/proguard-rules-desktop.pro b/application/composeApp/proguard-rules-desktop.pro index 9ccfe9b0d..7d38032bc 100644 --- a/application/composeApp/proguard-rules-desktop.pro +++ b/application/composeApp/proguard-rules-desktop.pro @@ -21,4 +21,5 @@ -dontwarn org.conscrypt.** -dontwarn org.bouncycastle.** -dontwarn org.openjsse.** +-dontwarn java.util.List From 05308760875aafff246a495836d879bf902e97cd Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Fri, 31 Oct 2025 15:17:52 +0100 Subject: [PATCH 39/39] apps alpha 45 --- application/composeApp/build.gradle.kts | 4 ++-- .../src/commonMain/composeResources/values-en/strings.xml | 2 +- .../src/commonMain/composeResources/values-pt/strings.xml | 2 +- .../src/commonMain/composeResources/values/strings.xml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/composeApp/build.gradle.kts b/application/composeApp/build.gradle.kts index e21070846..4bb2994dc 100644 --- a/application/composeApp/build.gradle.kts +++ b/application/composeApp/build.gradle.kts @@ -146,12 +146,12 @@ compose.desktop { } linux { - packageVersion = "0.44.0" + packageVersion = "0.45.0" iconFile.set(iconsRoot.resolve("icon-linux.png")) } windows { - packageVersion = "0.44.0" + packageVersion = "0.45.0" iconFile.set(iconsRoot.resolve("icon-windows.ico")) } } diff --git a/application/core/resources/src/commonMain/composeResources/values-en/strings.xml b/application/core/resources/src/commonMain/composeResources/values-en/strings.xml index 0356aeb24..2fc609105 100644 --- a/application/core/resources/src/commonMain/composeResources/values-en/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values-en/strings.xml @@ -54,7 +54,7 @@ Download Model Suggestions: Error when downloading model. - Alpha44 - Amado, Jorge + Alpha45 - Amado, Jorge Light Dark System diff --git a/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml b/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml index 7e020d706..6d57c017c 100644 --- a/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values-pt/strings.xml @@ -52,7 +52,7 @@ Baixar modelos Sugestões: Error ao baixar modelo. - "Alpha44 - Amado, Jorge" + "Alpha45 - Amado, Jorge" Claro Escuro Sistema diff --git a/application/core/resources/src/commonMain/composeResources/values/strings.xml b/application/core/resources/src/commonMain/composeResources/values/strings.xml index 0356aeb24..2fc609105 100644 --- a/application/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/application/core/resources/src/commonMain/composeResources/values/strings.xml @@ -54,7 +54,7 @@ Download Model Suggestions: Error when downloading model. - Alpha44 - Amado, Jorge + Alpha45 - Amado, Jorge Light Dark System