diff --git a/README.md b/README.md index cb3b8fe06..213fe89b3 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Currently Dicio answers questions about: - **media**: play, pause, previous, next song - **translation**: translate from/to any language with **Lingva** - _How do I say Football in German?_ - **wake word control**: turn on/off the wakeword - _Stop listening_ +- **nextcloud notes**: add notes to a Nextcloud instance, including a separate grocery list - _Take a note to implement Nextcloud support in Dicio_ ## Speech to text diff --git a/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt b/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt index 5c276aebc..06105ccf6 100644 --- a/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt +++ b/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt @@ -24,6 +24,7 @@ import org.stypox.dicio.skills.listening.ListeningInfo import org.stypox.dicio.skills.lyrics.LyricsInfo import org.stypox.dicio.skills.media.MediaInfo import org.stypox.dicio.skills.navigation.NavigationInfo +import org.stypox.dicio.skills.nextcloud_notes.NextcloudNotesInfo import org.stypox.dicio.skills.open.OpenInfo import org.stypox.dicio.skills.search.SearchInfo import org.stypox.dicio.skills.telephone.TelephoneInfo @@ -55,6 +56,7 @@ class SkillHandler @Inject constructor( JokeInfo, ListeningInfo(dataStore), TranslationInfo, + NextcloudNotesInfo, ) private val fallbackSkillInfoList = listOf( diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesInfo.kt b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesInfo.kt new file mode 100644 index 000000000..6ff1ab04a --- /dev/null +++ b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesInfo.kt @@ -0,0 +1,139 @@ +package org.stypox.dicio.skills.nextcloud_notes + +import android.content.Context +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Note +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.dataStore +import kotlinx.coroutines.launch +import org.dicio.skill.context.SkillContext +import org.dicio.skill.skill.Skill +import org.dicio.skill.skill.SkillInfo +import org.stypox.dicio.R +import org.stypox.dicio.sentences.Sentences +import org.stypox.dicio.settings.ui.StringSetting + +object NextcloudNotesInfo : SkillInfo("nextcloud_notes") { + override fun name(context: Context) = + context.getString(R.string.skill_name_nextcloud_notes) + + override fun sentenceExample(context: Context) = + context.getString(R.string.skill_sentence_example_nextcloud_notes) + + @Composable + override fun icon() = + rememberVectorPainter(Icons.Default.Note) + + override fun isAvailable(ctx: SkillContext): Boolean { + return Sentences.NextcloudNotes[ctx.sentencesLanguage] != null + } + + override fun build(ctx: SkillContext): Skill<*> { + return NextcloudNotesSkill(NextcloudNotesInfo, Sentences.NextcloudNotes[ctx.sentencesLanguage]!!) + } + + // DataStore for Nextcloud Notes settings + internal val Context.nextcloudNotesDataStore by dataStore( + fileName = "skill_settings_nextcloud_notes.pb", + serializer = SkillSettingsNextcloudNotesSerializer, + corruptionHandler = ReplaceFileCorruptionHandler { + SkillSettingsNextcloudNotesSerializer.defaultValue + }, + ) + + override val renderSettings: @Composable () -> Unit get() = @Composable { + val dataStore = LocalContext.current.nextcloudNotesDataStore + val data by dataStore.data.collectAsState(SkillSettingsNextcloudNotesSerializer.defaultValue) + val scope = rememberCoroutineScope() + + Column { + StringSetting( + title = stringResource(R.string.pref_nextcloud_notes_server_address), + descriptionWhenEmpty = stringResource(R.string.pref_nextcloud_notes_server_address_hint), + ).Render( + value = data.serverAddress, + onValueChange = { serverAddress -> + scope.launch { + dataStore.updateData { settings -> + settings.toBuilder() + .setServerAddress(serverAddress) + .build() + } + } + }, + ) + + StringSetting( + title = stringResource(R.string.pref_nextcloud_notes_username), + descriptionWhenEmpty = stringResource(R.string.pref_nextcloud_notes_username_hint), + ).Render( + value = data.username, + onValueChange = { username -> + scope.launch { + dataStore.updateData { settings -> + settings.toBuilder() + .setUsername(username) + .build() + } + } + }, + ) + + StringSetting( + title = stringResource(R.string.pref_nextcloud_notes_password), + descriptionWhenEmpty = stringResource(R.string.pref_nextcloud_notes_password_hint), + ).Render( + value = data.password, + onValueChange = { password -> + scope.launch { + dataStore.updateData { settings -> + settings.toBuilder() + .setPassword(password) + .build() + } + } + }, + ) + + StringSetting( + title = stringResource(R.string.pref_nextcloud_notes_target_note), + descriptionWhenEmpty = stringResource(R.string.pref_nextcloud_notes_target_note_hint), + ).Render( + value = data.targetNote, + onValueChange = { targetNote -> + scope.launch { + dataStore.updateData { settings -> + settings.toBuilder() + .setTargetNote(targetNote) + .build() + } + } + }, + ) + + StringSetting( + title = stringResource(R.string.pref_nextcloud_notes_target_shopping_list), + descriptionWhenEmpty = stringResource(R.string.pref_nextcloud_notes_target_shopping_list_hint), + ).Render( + value = data.targetShoppingList, + onValueChange = { targetShoppingList -> + scope.launch { + dataStore.updateData { settings -> + settings.toBuilder() + .setTargetShoppingList(targetShoppingList) + .build() + } + } + }, + ) + } + } +} diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesOutput.kt b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesOutput.kt new file mode 100644 index 000000000..52a25142c --- /dev/null +++ b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesOutput.kt @@ -0,0 +1,147 @@ +package org.stypox.dicio.skills.nextcloud_notes + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.dicio.skill.context.SkillContext +import org.dicio.skill.skill.SkillOutput +import org.stypox.dicio.R +import org.stypox.dicio.io.graphical.HeadlineSpeechSkillOutput +import org.stypox.dicio.util.getString + +sealed interface NextcloudNotesOutput : SkillOutput { + data class Success( + val noteName: String, + val content: String, + val isShoppingList: Boolean, + ) : NextcloudNotesOutput, HeadlineSpeechSkillOutput { + override fun getSpeechOutput(ctx: SkillContext): String = if (isShoppingList) { + ctx.getString(R.string.skill_nextcloud_notes_added_to_shopping_list, content) + } else { + ctx.getString(R.string.skill_nextcloud_notes_note_added, content) + } + + @Composable + override fun GraphicalOutput(ctx: SkillContext) { + Column { + Text( + text = if (isShoppingList) { + stringResource(R.string.skill_nextcloud_notes_added_to_shopping_list, content) + } else { + stringResource(R.string.skill_nextcloud_notes_note_added, content) + }, + style = MaterialTheme.typography.bodyLarge, + modifier = androidx.compose.ui.Modifier.testTag("nextcloud_notes_success") + ) + Text( + text = stringResource(R.string.skill_nextcloud_notes_saved_to, noteName), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + data class QueryShoppingListSuccess( + val noteName: String, + val items: List + ) : NextcloudNotesOutput, HeadlineSpeechSkillOutput { + override fun getSpeechOutput(ctx: SkillContext): String { + return if (items.isEmpty()) { + ctx.getString(R.string.skill_nextcloud_notes_shopping_list_empty) + } else { + val itemsText = items.joinToString(", ") + ctx.getString(R.string.skill_nextcloud_notes_shopping_list_items, items.size, itemsText) + } + } + + @Composable + override fun GraphicalOutput(ctx: SkillContext) { + Column { + Text( + text = if (items.isEmpty()) { + stringResource(R.string.skill_nextcloud_notes_shopping_list_empty) + } else { + stringResource(R.string.skill_nextcloud_notes_shopping_list_num_of_items, items.size) + }, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.testTag("nextcloud_notes_shopping_list_title") + ) + if (items.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + items.forEachIndexed { index, item -> + Text( + text = "- $item", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.testTag("nextcloud_notes_shopping_item_$index") + ) + } + } + Text( + text = stringResource(R.string.skill_nextcloud_notes_saved_to, noteName), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + data class ItemFound( + val item: String, + val found: Boolean + ) : NextcloudNotesOutput, HeadlineSpeechSkillOutput { + override fun getSpeechOutput(ctx: SkillContext): String { + return if (found) { + ctx.getString(R.string.skill_nextcloud_notes_item_found, item) + } else { + ctx.getString(R.string.skill_nextcloud_notes_item_not_found, item) + } + } + + @Composable + override fun GraphicalOutput(ctx: SkillContext) { + Column { + Text( + text = if (found) { + stringResource(R.string.skill_nextcloud_notes_item_found, item) + } else { + stringResource(R.string.skill_nextcloud_notes_item_not_found, item) + }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.testTag("nextcloud_notes_item_check") + ) + } + } + } + + data class Failed( + val reason: FailureReason, + val errorMessage: String? = null + ) : NextcloudNotesOutput, HeadlineSpeechSkillOutput { + override fun getSpeechOutput(ctx: SkillContext): String = when (reason) { + FailureReason.SETTINGS_MISSING -> ctx.getString(R.string.skill_nextcloud_notes_settings_missing) + FailureReason.TARGET_NOTE_MISSING -> ctx.getString(R.string.skill_nextcloud_notes_target_note_missing) + FailureReason.TARGET_SHOPPING_LIST_MISSING -> ctx.getString(R.string.skill_nextcloud_notes_target_shopping_list_missing) + FailureReason.CONTENT_EMPTY -> ctx.getString(R.string.skill_nextcloud_notes_content_empty) + FailureReason.CONNECTION_ERROR -> ctx.getString( + R.string.skill_nextcloud_notes_connection_error, + errorMessage ?: ctx.getString(R.string.skill_nextcloud_notes_unknown_error) + ) + } + } + + enum class FailureReason { + SETTINGS_MISSING, + TARGET_NOTE_MISSING, + TARGET_SHOPPING_LIST_MISSING, + CONTENT_EMPTY, + CONNECTION_ERROR + } +} diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesSkill.kt b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesSkill.kt new file mode 100644 index 000000000..fb702c2b9 --- /dev/null +++ b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/NextcloudNotesSkill.kt @@ -0,0 +1,310 @@ +package org.stypox.dicio.skills.nextcloud_notes + +import kotlinx.coroutines.flow.first +import okhttp3.Credentials +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.dicio.skill.context.SkillContext +import org.dicio.skill.skill.SkillInfo +import org.dicio.skill.skill.SkillOutput +import org.dicio.skill.standard.StandardRecognizerData +import org.dicio.skill.standard.StandardRecognizerSkill +import org.stypox.dicio.sentences.Sentences.NextcloudNotes +import org.stypox.dicio.skills.nextcloud_notes.NextcloudNotesInfo.nextcloudNotesDataStore +import org.stypox.dicio.util.StringUtils +import java.net.URLEncoder +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class NextcloudNotesSkill( + correspondingSkillInfo: SkillInfo, + data: StandardRecognizerData +) : StandardRecognizerSkill(correspondingSkillInfo, data) { + + override suspend fun generateOutput(ctx: SkillContext, inputData: NextcloudNotes): SkillOutput { + val prefs = ctx.android.nextcloudNotesDataStore.data.first() + + if (prefs.serverAddress.isEmpty() || prefs.username.isEmpty() || prefs.password.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.SETTINGS_MISSING + ) + } + + return when (inputData) { + is NextcloudNotes.AddNote -> { + if (prefs.targetNote.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.TARGET_NOTE_MISSING + ) + } + val content = inputData.content ?: "" + if (content.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.CONTENT_EMPTY + ) + } + + try { + addNoteViaWebDAV( + serverAddress = prefs.serverAddress, + username = prefs.username, + password = prefs.password, + noteName = prefs.targetNote, + content = content, + isShoppingList = false + ) + return NextcloudNotesOutput.Success( + noteName = prefs.targetNote, + content = content, + isShoppingList = false + ) + } catch (e: Exception) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.CONNECTION_ERROR, + errorMessage = e.message + ) + } + } + + is NextcloudNotes.AddToShoppingList -> { + if (prefs.targetShoppingList.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.TARGET_SHOPPING_LIST_MISSING + ) + } + val content = inputData.item ?: "" + if (content.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.CONTENT_EMPTY + ) + } + + try { + addNoteViaWebDAV( + serverAddress = prefs.serverAddress, + username = prefs.username, + password = prefs.password, + noteName = prefs.targetShoppingList, + content = content, + isShoppingList = true + ) + return NextcloudNotesOutput.Success( + noteName = prefs.targetShoppingList, + content = content, + isShoppingList = true + ) + } catch (e: Exception) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.CONNECTION_ERROR, + errorMessage = e.message + ) + } + } + + is NextcloudNotes.QueryShoppingList -> { + if (prefs.targetShoppingList.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.TARGET_SHOPPING_LIST_MISSING + ) + } + + try { + return queryShoppingList( + serverAddress = prefs.serverAddress, + username = prefs.username, + password = prefs.password, + noteName = prefs.targetShoppingList + ) + } catch (e: Exception) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.CONNECTION_ERROR, + errorMessage = e.message + ) + } + } + + is NextcloudNotes.CheckShoppingList -> { + if (prefs.targetShoppingList.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.TARGET_SHOPPING_LIST_MISSING + ) + } + val item = inputData.item ?: "" + if (item.isEmpty()) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.CONTENT_EMPTY + ) + } + + try { + return checkShoppingListItem( + serverAddress = prefs.serverAddress, + username = prefs.username, + password = prefs.password, + noteName = prefs.targetShoppingList, + itemToCheck = item + ) + } catch (e: Exception) { + return NextcloudNotesOutput.Failed( + reason = NextcloudNotesOutput.FailureReason.CONNECTION_ERROR, + errorMessage = e.message + ) + } + } + } + } + + private fun buildNoteUrl(serverAddress: String, username: String, noteName: String): String { + val normalizedServer = serverAddress.trimEnd('/') + val encodedNoteName = URLEncoder.encode(noteName, "UTF-8") + return "$normalizedServer/remote.php/dav/files/$username/Notes/$encodedNoteName" + } + + private suspend fun addNoteViaWebDAV( + serverAddress: String, + username: String, + password: String, + noteName: String, + content: String, + isShoppingList: Boolean + ) { + val client = OkHttpClient() + val noteUrl = buildNoteUrl(serverAddress, username, noteName) + + // Get existing content first + val getRequest = Request.Builder() + .url(noteUrl) + .header("Authorization", Credentials.basic(username, password)) + .get() + .build() + + val existingContent = try { + val response = client.newCall(getRequest).execute() + if (response.isSuccessful) { + response.body?.string() ?: "" + } else { + "" + } + } catch (_: Exception) { + "" + } + + // Append new content with timestamp + val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date()) + val newContent = if (isShoppingList) { + // For shopping lists, add as a checkbox item + if (existingContent.isEmpty()) { + "- [ ] $content" + } else { + "$existingContent\n- [ ] $content" + } + } else { + // For notes, add with timestamp + if (existingContent.isEmpty()) { + "[$timestamp] $content" + } else { + "$existingContent\n\n[$timestamp] $content" + } + } + + val putRequest = Request.Builder() + .url(noteUrl) + .header("Authorization", Credentials.basic(username, password)) + .put(newContent.toRequestBody("text/plain".toMediaType())) + .build() + + val response = client.newCall(putRequest).execute() + if (!response.isSuccessful) { + throw Exception("Failed to add note: ${response.code} ${response.message}") + } + } + + private suspend fun queryShoppingList( + serverAddress: String, + username: String, + password: String, + noteName: String + ): NextcloudNotesOutput { + val content = fetchNoteContent(serverAddress, username, password, noteName) + val items = parseShoppingListItems(content) + + return NextcloudNotesOutput.QueryShoppingListSuccess( + noteName = noteName, + items = items + ) + } + + private suspend fun checkShoppingListItem( + serverAddress: String, + username: String, + password: String, + noteName: String, + itemToCheck: String + ): NextcloudNotesOutput { + val content = fetchNoteContent(serverAddress, username, password, noteName) + val items = parseShoppingListItems(content) + + val matchedItem = items.firstOrNull { item -> + StringUtils.customStringDistance(item, itemToCheck.trim()) <= 3 + } + + return NextcloudNotesOutput.ItemFound( + item = matchedItem ?: itemToCheck.trim(), + found = matchedItem != null + ) + } + + private suspend fun fetchNoteContent( + serverAddress: String, + username: String, + password: String, + noteName: String + ): String { + val client = OkHttpClient() + val noteUrl = buildNoteUrl(serverAddress, username, noteName) + + val getRequest = Request.Builder() + .url(noteUrl) + .header("Authorization", Credentials.basic(username, password)) + .get() + .build() + + val response = client.newCall(getRequest).execute() + if (!response.isSuccessful) { + throw Exception("Failed to fetch note: ${response.code} ${response.message}") + } + + return response.body?.string() ?: "" + } + + private fun parseShoppingListItems(content: String): List { + if (content.isBlank()) return emptyList() + + val checkboxRegex = Regex("^\\s*-\\s*\\[.?\\]\\s*(.+)$") + + return content.lines() + .mapNotNull { line -> + checkboxRegex.find(line)?.let { match -> + stripMarkdownForSpeech(match.groupValues[1].trim()) + .takeIf { it.isNotBlank() } + } + } + } + + private fun stripMarkdownForSpeech(text: String): String { + return text + // Remove markdown headers + .replace(Regex("^#{1,6}\\s+"), "") + // Remove bold/italic markers + .replace(Regex("[*_]{1,2}"), "") + // Remove links but keep text: [text](url) -> text + .replace(Regex("\\[([^]]+)\\]\\([^)]+\\)"), "$1") + // Remove inline code markers + .replace(Regex("`([^`]+)`"), "$1") + // Clean up extra whitespace + .trim() + } +} diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/SkillSettingsNextcloudNotesSerializer.kt b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/SkillSettingsNextcloudNotesSerializer.kt new file mode 100644 index 000000000..4f1ea5986 --- /dev/null +++ b/app/src/main/kotlin/org/stypox/dicio/skills/nextcloud_notes/SkillSettingsNextcloudNotesSerializer.kt @@ -0,0 +1,23 @@ +package org.stypox.dicio.skills.nextcloud_notes + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +object SkillSettingsNextcloudNotesSerializer : Serializer { + override val defaultValue: SkillSettingsNextcloudNotes = SkillSettingsNextcloudNotes.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): SkillSettingsNextcloudNotes { + try { + return SkillSettingsNextcloudNotes.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto", exception) + } + } + + override suspend fun writeTo(t: SkillSettingsNextcloudNotes, output: OutputStream) { + t.writeTo(output) + } +} diff --git a/app/src/main/proto/skill_settings_nextcloud_notes.proto b/app/src/main/proto/skill_settings_nextcloud_notes.proto new file mode 100644 index 000000000..67f194e06 --- /dev/null +++ b/app/src/main/proto/skill_settings_nextcloud_notes.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option java_package = "org.stypox.dicio.skills.nextcloud_notes"; +option java_multiple_files = true; + +message SkillSettingsNextcloudNotes { + string server_address = 1; + string username = 2; + string password = 3; + string target_note = 4; + string target_shopping_list = 5; +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8230af59d..e412a5724 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -246,4 +246,30 @@ Failed to copy to clipboard Auto DuckDuckGo did not provide results, asking for a Captcha to be solved + Nextcloud Notes + Add a note to buy milk + Added note: %1$s + Added to shopping list: %1$s + Saved to: %1$s + Nextcloud server settings are not configured. Please configure them in settings. + Target note name is not configured. Please configure it in settings. + Target shopping list name is not configured. Please configure it in settings. + The note content cannot be empty. + Failed to connect to Nextcloud: %1$s + Unknown error + Your shopping list has %1$d items + You have %1$d items to get: %2$s + Your shopping list is empty + Yes, %1$s is on your shopping list + No, %1$s is not on your shopping list + Nextcloud server address + e.g., https://cloud.example.com + Username + Your Nextcloud username + Nextcloud App Password + For security, use an app password + Target note name + Note filename with extension (e.g., MyNotes.md or notes.txt) + Shopping list note name + Shopping list filename with extension (e.g., Shopping.md) diff --git a/app/src/main/sentences/en/nextcloud_notes.yml b/app/src/main/sentences/en/nextcloud_notes.yml new file mode 100644 index 000000000..1dfefec4b --- /dev/null +++ b/app/src/main/sentences/en/nextcloud_notes.yml @@ -0,0 +1,27 @@ +add_note: + - (could|can|would) you? please? (add|take|make|create|write) a note .content. + - (i d|would) like you? to? (add|take|make|write) a note .content. + - please? (add|take|make|create|write) a note .content. + - (please? take|make) a note (of|that|about)? .content. + +add_to_shopping_list: + - (could|can|would) you? please? (add|put) .item. (to|on|in) (my|our|the)? (shopping|grocery) list + - (i d|would) like you? to? (add|put) .item. (to|on) (my|our|the)? (shopping|grocery) list + - (shopping|grocery) list add .item. + - (i|we) need to (get|buy|pick up|purchase) .item. + - (i|we) need .item. (from the store|on the list)? + - please? (add|put|write) .item. (on|to|in) (my|our|the)? (shopping|grocery|groceries)? list? + +query_shopping_list: + - (what|what s|what item? is|what items? (do|are)) (written|noted|I have|we have)? on (my|our|the)? (shopping|grocery) list + - (what do|what did) (i|we) need (to (get|buy|pick up|purchase))? + - read (my|our|the)? (shopping|grocery) list + - (show|tell) me (my|our|the)? (shopping|grocery) list + - (shopping|grocery) list + +check_shopping_list: + - (do|did) (i|we) (add|put|need) .item. (to|on) (my|our|the)? (shopping|grocery) list + - (do|did) (i|we) (need|have) .item. (on the list)? + - is .item. on (my|our|the)? (shopping|grocery) list + - is .item. (already)? (there|on there) + - check (for|if) .item. (is)? (on|in) (my|our|the)? (shopping|grocery)? list diff --git a/app/src/main/sentences/skill_definitions.yml b/app/src/main/sentences/skill_definitions.yml index 7aa23f72f..9c2ac3b64 100644 --- a/app/src/main/sentences/skill_definitions.yml +++ b/app/src/main/sentences/skill_definitions.yml @@ -123,3 +123,20 @@ skills: type: string - id: target type: string + + - id: nextcloud_notes + specificity: high + sentences: + - id: add_note + captures: + - id: content + type: string + - id: add_to_shopping_list + captures: + - id: item + type: string + - id: query_shopping_list + - id: check_shopping_list + captures: + - id: item + type: string diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index f572451fc..1beb336bc 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -14,6 +14,7 @@ Dicio answers questions about:
  • jokes: tells you a joke - Tell me a joke
  • media: play, pause, previous, next song - Next Song
  • translation: translate from/to any language with Lingva - How do I say Football in German?
  • +
  • nextcloud notes: add notes to a Nextcloud instance, including a separate grocery list - Take a note to implement Nextcloud support in Dicio
  • Dicio can receive input through a text box or through Vosk speech to text, and can talk using toasts or the Android speech synthesis engine. Interactive graphical output is provided by skills when they answer a question.