From 08bf38722a0ddf86e6556740807adb9bf49308a5 Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Fri, 14 Aug 2026 19:33:09 -0700 Subject: [PATCH] chore: publish Quickstarts for SDK version 5.1.0 --- .env.sample | 9 +- .gitattributes | 19 + .../actions/seed-ditto-document/action.yml | 27 +- .github/browserstack-devices.yml | 82 +- AGENTS.md | 1 + LICENSE | 2 +- README.md | 11 +- android-java/README.md | 15 +- android-java/app/build.gradle.kts | 28 +- .../com/example/dittotasks/DittoHelper.kt | 4 +- .../com/example/dittotasks/MainActivity.java | 34 +- .../app/src/main/res/layout/activity_main.xml | 4 +- .../app/src/main/res/values/strings.xml | 4 +- android-java/gradle/libs.versions.toml | 2 +- .../QuickStartTasks/app/build.gradle.kts | 12 +- .../quickstart/tasks/TasksApplication.kt | 6 +- .../tasks/edit/EditScreenViewModel.kt | 4 +- .../quickstart/tasks/list/TasksListScreen.kt | 8 +- .../tasks/list/TasksListScreenViewModel.kt | 57 +- .../QuickStartTasks/gradle/libs.versions.toml | 2 +- android-kotlin/README.md | 12 +- cpp-tui/README.md | 6 +- cpp-tui/taskscpp/Makefile | 2 +- cpp-tui/taskscpp/src/main.cpp | 71 +- cpp-tui/taskscpp/src/tasks_log.cpp | 30 +- cpp-tui/taskscpp/src/tasks_log.h | 3 - cpp-tui/taskscpp/src/tasks_peer.cpp | 145 +- cpp-tui/taskscpp/src/tasks_peer.h | 16 +- cpp-tui/taskscpp/src/tasks_tui.cpp | 4 +- cpp-tui/taskscpp/tests/integration_test.cpp | 142 +- cpp-tui/taskscpp/tests/run_tests.sh | 2 +- .../DittoMauiTasksApp.csproj | 134 +- dotnet-maui/DittoMauiTasksApp/MauiProgram.cs | 246 +-- .../Platforms/MacCatalyst/AppDelegate.cs | 2 +- .../Platforms/MacCatalyst/Program.cs | 2 +- .../DittoMauiTasksApp/Utils/PopupService.cs | 28 +- .../ViewModels/TasksPageviewModel.cs | 688 ++++---- .../DittoMauiTasksApp/Views/TasksPage.xaml | 186 +-- .../DittoMauiTasksApp/Views/TasksPage.xaml.cs | 62 +- dotnet-maui/README.md | 6 +- .../UITests.Android/AndroidTaskSearchTests.cs | 172 +- dotnet-maui/UITests.Android/Program.cs | 2 +- dotnet-maui/UITests.Shared/BaseTest.cs | 246 +-- dotnet-maui/UITests.Shared/TaskSearchTests.cs | 174 +- dotnet-maui/UITests.iOS/Program.cs | 2 +- dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs | 254 +-- .../DittoDotNetTasksConsole.Tests.csproj | 2 +- .../IntegrationTests.cs | 11 +- .../DittoDotNetTasksConsole.csproj | 2 +- dotnet-tui/DittoDotNetTasksConsole/Program.cs | 9 +- .../DittoDotNetTasksConsole/TasksPeer.cs | 118 +- .../DittoDotNetTasksConsole/TasksWindow.cs | 4 +- dotnet-tui/README.md | 6 +- dotnet-winforms-net48/.env.sample | 14 +- dotnet-winforms-net48/App.config | 34 +- dotnet-winforms-net48/AppConfiguration.cs | 30 +- dotnet-winforms-net48/MainForm.Designer.cs | 328 ++-- dotnet-winforms-net48/MainForm.cs | 506 +++--- dotnet-winforms-net48/MainForm.resx | 238 +-- dotnet-winforms-net48/Program.cs | 136 +- .../Properties/AssemblyInfo.cs | 66 +- .../Properties/Resources.Designer.cs | 142 +- .../Properties/Resources.resx | 232 +-- .../Properties/Settings.Designer.cs | 60 +- .../Properties/Settings.settings | 14 +- dotnet-winforms-net48/README.md | 6 +- .../Taskapp.WinForms.Net48.csproj | 319 ++-- .../Taskapp.WinForms.Net48.sln | 50 +- dotnet-winforms-net48/TasksPeer.cs | 536 +++--- dotnet-winforms-net48/TasksPeerService.cs | 4 +- dotnet-winforms-net48/ToDoTask.cs | 58 +- dotnet-winforms-net48/packages.config | 40 +- dotnet-winforms/IntegrationTest/Program.cs | 17 +- dotnet-winforms/README.md | 6 +- .../TasksApp/AboutForm.Designer.cs | 182 +- dotnet-winforms/TasksApp/AboutForm.cs | 7 +- dotnet-winforms/TasksApp/DittoTasksApp.csproj | 2 +- dotnet-winforms/TasksApp/MainForm.Designer.cs | 16 +- dotnet-winforms/TasksApp/MainForm.cs | 4 +- dotnet-winforms/TasksApp/Program.cs | 11 +- dotnet-winforms/TasksApp/TasksPeer.cs | 116 +- electron/README.md | 13 +- electron/package-lock.json | 1274 ++++++++------ electron/package.json | 12 +- electron/src/main/ditto.ts | 20 +- electron/src/main/env.ts | 17 +- electron/src/renderer/App.tsx | 6 +- .../src/renderer/components/DittoInfo.tsx | 6 +- electron/src/types.ts | 2 +- electron/tailwind.config.js | 5 +- electron/tsconfig.web.json | 6 +- flutter_app/README.md | 6 +- flutter_app/android/build.gradle | 6 + flutter_app/android/gradlew.bat | 188 +-- flutter_app/integration_test/app_test.dart | 25 +- flutter_app/ios/Podfile.lock | 6 +- flutter_app/lib/dialog.dart | 20 +- flutter_app/lib/dql_builder.dart | 13 +- flutter_app/lib/main.dart | 75 +- flutter_app/macos/Podfile.lock | 6 +- flutter_app/pubspec.lock | 6 +- flutter_app/pubspec.yaml | 14 +- flutter_app/test/dql_safety_test.dart | 71 + flutter_app/test/widget_test.dart | 16 +- flutter_app/windows/CMakeLists.txt | 8 + global.json | 7 + go-tui/README.md | 16 +- go-tui/main.go | 20 +- java-server/README.md | 6 +- java-server/build.gradle.kts | 10 +- java-server/java-spring-maven/pom.xml | 4 +- java-server/settings.gradle.kts | 2 +- .../controller/TaskRestController.java | 4 +- .../quickstart/service/DittoService.java | 6 +- .../quickstart/service/DittoTaskService.java | 40 +- .../templates/fragments/taskList.html | 2 +- javascript-tui/.prettierignore | 1 + javascript-tui/README.md | 22 +- javascript-tui/package-lock.json | 8 +- javascript-tui/package.json | 130 +- javascript-tui/prettier.config.cjs | 7 + javascript-tui/prettier.config.js | 9 - javascript-tui/source/app.js | 598 +++---- javascript-tui/source/cli.js | 151 +- javascript-tui/tests/integration_test.js | 151 +- javascript-web/README.md | 16 +- javascript-web/package-lock.json | 1473 ++++++++--------- javascript-web/package.json | 6 +- javascript-web/src/App.tsx | 31 +- javascript-web/src/components/DittoInfo.tsx | 6 +- kotlin-multiplatform/README.md | 6 +- .../composeApp/build.gradle.kts | 10 +- .../quickstart/ditto/DittoManager.android.kt | 2 - .../ditto/quickstart/data/DittoCredentials.kt | 2 +- .../data/repository/DittoTaskRepository.kt | 35 +- .../data/screenstate/MainScreenState.kt | 4 +- .../ditto/quickstart/ditto/DittoManager.kt | 39 +- .../com/ditto/quickstart/ui/MainScreen.kt | 2 +- .../quickstart/ui/MainScreenViewModel.kt | 4 +- .../ditto/quickstart/ui/components/TopBar.kt | 6 +- .../usecases/SetSyncStatusUseCase.kt | 6 +- kotlin-multiplatform/detekt.yml | 133 +- ...1-app-launch-and-seeded-tasks-android.yaml | 12 +- .../01-app-launch-and-seeded-tasks-ios.yaml | 14 +- react-native-expo/App.tsx | 235 +-- react-native-expo/README.md | 2 +- react-native-expo/android/gradle.properties | 9 +- react-native-expo/babel.config.js | 8 +- react-native-expo/components/DittoInfo.tsx | 10 +- .../components/EditTaskModal.tsx | 25 +- react-native-expo/components/Fab.tsx | 7 +- react-native-expo/components/NewTaskModal.tsx | 22 +- react-native-expo/components/TaskDone.tsx | 6 +- react-native-expo/eslint.config.js | 6 +- react-native-expo/main.tsx | 4 +- react-native-expo/metro.config.js | 2 +- react-native-expo/package-lock.json | 52 +- react-native-expo/package.json | 16 +- react-native-expo/scripts/reset-project.js | 42 +- react-native-expo/types/env.d.ts | 8 +- react-native-expo/yarn.lock | 66 +- ...1-app-launch-and-seeded-tasks-android.yaml | 10 +- .../01-app-launch-and-seeded-tasks-ios.yaml | 14 +- react-native/App.tsx | 110 +- react-native/Gemfile | 2 +- react-native/Gemfile.lock | 4 +- react-native/README.md | 6 +- react-native/__mocks__/@env.js | 6 +- .../android/app/src/main/AndroidManifest.xml | 5 + react-native/android/gradle.properties | 9 +- react-native/components/DittoInfo.tsx | 4 +- react-native/components/DittoSync.tsx | 6 +- react-native/components/EditTaskModal.tsx | 4 +- react-native/components/NewTaskModal.tsx | 4 +- react-native/components/TaskDone.tsx | 4 +- react-native/ios/Podfile | 15 + react-native/jest.setup.js | 14 +- react-native/package-lock.json | 30 +- react-native/package.json | 10 +- react-native/types/env.d.ts | 6 +- react-native/yarn.lock | 26 +- rust-tui/.cargo/config.toml | 10 + rust-tui/Cargo.lock | 27 +- rust-tui/Cargo.toml | 2 +- rust-tui/README.md | 12 +- rust-tui/clippy.toml | 11 + rust-tui/src/bin/integration_test.rs | 40 +- rust-tui/src/bin/main.rs | 83 +- rust-tui/src/lib.rs | 7 +- rust-tui/src/term.rs | 5 +- rust-tui/src/tui/mod.rs | 10 +- rust-tui/src/tui/todolist.rs | 50 +- scripts/README.md | 2 - scripts/bump-versions.js | 412 ----- swift/.swiftlint.yml | 2 +- swift/README.md | 6 +- swift/Tasks.xcodeproj/project.pbxproj | 10 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- swift/Tasks/DittoManager.swift | 16 +- swift/Tasks/TasksListScreen.swift | 68 +- swift/buildEnv.sh | 27 +- 201 files changed, 6192 insertions(+), 6537 deletions(-) create mode 100644 .gitattributes create mode 120000 AGENTS.md create mode 100644 flutter_app/test/dql_safety_test.dart create mode 100644 global.json create mode 100644 javascript-tui/prettier.config.cjs delete mode 100644 javascript-tui/prettier.config.js create mode 100644 rust-tui/.cargo/config.toml create mode 100644 rust-tui/clippy.toml delete mode 100644 scripts/README.md delete mode 100755 scripts/bump-versions.js diff --git a/.env.sample b/.env.sample index aab8aa552..d65021c1c 100644 --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,7 @@ #!/usr/bin/env bash # Copy this file from ".env.sample" to ".env", then fill in these values -# A Ditto AppID and Playground token can be obtained from https://portal.ditto.live -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" -DITTO_WEBSOCKET_URL="" \ No newline at end of file +# A Ditto Database ID and development token can be obtained from https://portal.ditto.live +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f1ee23674 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Line-ending policy for the published Quickstart apps. +# +# The publish step (scripts/ci/quickstart/publish_quickstart.sh) exports this +# subtree with `git archive HEAD:quickstart`, which resolves .gitattributes +# relative to THIS directory. Without a .gitattributes here, the monorepo-root +# .gitattributes EOL rules are not applied to the published output, so every +# file is exported with LF -- dropping the CRLF that Windows batch files +# require. These rules are a stricter LF-forcing variant of the root policy +# (a blanket `eol=lf` rather than the root's platform-dependent `text=auto`, +# plus an explicit `*.cmd` rule) so the published getditto/quickstart repo +# (and this subtree) get deterministic, convention-correct line endings. + +# Normalize all text to LF in the repository and on checkout/export +# (matches the repo-root .editorconfig `end_of_line = lf`). +* text=auto eol=lf + +# Windows batch/command scripts require CRLF for cmd.exe. +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.github/actions/seed-ditto-document/action.yml b/.github/actions/seed-ditto-document/action.yml index cad09b91d..179567a47 100644 --- a/.github/actions/seed-ditto-document/action.yml +++ b/.github/actions/seed-ditto-document/action.yml @@ -3,12 +3,16 @@ description: 'Creates a test document in Ditto Cloud with inverted timestamp for author: 'Ditto' inputs: - ditto-api-key: - description: 'Ditto API key for authentication' + ditto-app-id: + description: 'Ditto App ID' required: true - ditto-api-url: - description: 'Ditto API URL (e.g., cloud.ditto.live)' + ditto-development-token: + description: 'Ditto Development Token for authentication' required: true + ditto-api-url: + description: 'Ditto Cloud URL Endpoint host (no scheme), e.g. .cloud.ditto.live. Overrides the default {app-id}.cloud.ditto.live host when set.' + required: false + default: '' app-name: description: 'Name of the app/platform being tested (e.g., android-kotlin, swift, etc.)' required: true @@ -31,6 +35,7 @@ runs: id: seed shell: bash run: | + set -euo pipefail # Generate inverted timestamp for top position in list TIMESTAMP=$(date +%s) INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) @@ -45,9 +50,11 @@ runs: echo "πŸ“ Timestamp: ${TIMESTAMP} β†’ Inverted: ${INVERTED_TIMESTAMP}" # Insert document using Ditto API v4 - RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + DITTO_HOST="${{ inputs.ditto-api-url }}" + DITTO_HOST="${DITTO_HOST:-${{ inputs.ditto-app-id }}.cloud.ditto.live}" + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST \ -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ inputs.ditto-api-key }}" \ + -H "Authorization: Bearer ${{ inputs.ditto-development-token }}" \ -d "{ \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", \"args\": { @@ -59,7 +66,7 @@ runs: } } }" \ - "https://${{ inputs.ditto-api-url }}/api/v4/store/execute") + "https://${DITTO_HOST}/api/v4/store/execute") # Extract HTTP status code and response body (portable version) HTTP_CODE=$(echo "$RESPONSE" | tail -n1) @@ -68,9 +75,9 @@ runs: # Check if insertion was successful if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then echo "βœ… Successfully inserted test document" - echo "document_id=${DOC_ID}" >> $GITHUB_OUTPUT - echo "document_title=${DOC_TITLE}" >> $GITHUB_OUTPUT - echo "inverted_timestamp=${INVERTED_TIMESTAMP}" >> $GITHUB_OUTPUT + echo "document_id=${DOC_ID}" >> "$GITHUB_OUTPUT" + echo "document_title=${DOC_TITLE}" >> "$GITHUB_OUTPUT" + echo "inverted_timestamp=${INVERTED_TIMESTAMP}" >> "$GITHUB_OUTPUT" else echo "❌ Failed to insert document. HTTP Status: $HTTP_CODE" echo "Response: $BODY" diff --git a/.github/browserstack-devices.yml b/.github/browserstack-devices.yml index 98b14892f..721a051de 100644 --- a/.github/browserstack-devices.yml +++ b/.github/browserstack-devices.yml @@ -16,95 +16,95 @@ # Android Java QuickStart android-java: devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship, Android 13 - - "Google Pixel 6-12.0" # Android 12 support - - "OnePlus 9-11.0" # Android 11 minimum baseline + - 'Google Pixel 8-14.0' # Latest Android 14 + - 'Samsung Galaxy S23-13.0' # Popular flagship, Android 13 + - 'Google Pixel 6-12.0' # Android 12 support + - 'OnePlus 9-11.0' # Android 11 minimum baseline # Android Kotlin QuickStart android-kotlin: devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship, Android 13 - - "Google Pixel 6-12.0" # Android 12 support - - "OnePlus 9-11.0" # Android 11 minimum baseline + - 'Google Pixel 8-14.0' # Latest Android 14 + - 'Samsung Galaxy S23-13.0' # Popular flagship, Android 13 + - 'Google Pixel 6-12.0' # Android 12 support + - 'OnePlus 9-11.0' # Android 11 minimum baseline # Flutter QuickStart (multi-platform) flutter: android: devices: - - "Google Pixel 7-13.0" # Android 13 baseline - - "Samsung Galaxy S23-13.0" # Popular flagship - - "Google Pixel 6-12.0" # Android 12 support + - 'Google Pixel 7-13.0' # Android 13 baseline + - 'Samsung Galaxy S23-13.0' # Popular flagship + - 'Google Pixel 6-12.0' # Android 12 support ios: devices: - - "iPhone 13-16" # iOS 16 support (min deployment target is 15.6) - - "iPhone 14-16" # iOS 16 current - - "iPhone 12-17" # iOS 17 latest + - 'iPhone 13-16' # iOS 16 support (min deployment target is 15.6) + - 'iPhone 14-16' # iOS 16 current + - 'iPhone 12-17' # iOS 17 latest # Kotlin Multiplatform QuickStart kotlin-multiplatform: android: devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship + - 'Google Pixel 8-14.0' # Latest Android 14 + - 'Samsung Galaxy S23-13.0' # Popular flagship # React Native QuickStart react-native: android: devices: - - "Samsung Galaxy S22-12.0" # Android 12 - - "Google Pixel 7-13.0" # Android 13 + - 'Samsung Galaxy S22-12.0' # Android 12 + - 'Google Pixel 7-13.0' # Android 13 ios: devices: - - "iPhone 15-17.0" # Latest iOS 17 - - "iPhone 14-16.0" # iOS 16 support + - 'iPhone 15-17.0' # Latest iOS 17 + - 'iPhone 14-16.0' # iOS 16 support # React Native Expo QuickStart react-native-expo: android: devices: - - "Samsung Galaxy S22-12.0" # Android 12 - - "Google Pixel 7-13.0" # Android 13 + - 'Samsung Galaxy S22-12.0' # Android 12 + - 'Google Pixel 7-13.0' # Android 13 ios: devices: - - "iPhone 15-17.0" # Latest iOS 17 - - "iPhone 14-16.0" # iOS 16 support + - 'iPhone 15-17.0' # Latest iOS 17 + - 'iPhone 14-16.0' # iOS 16 support # .NET MAUI QuickStart (multi-platform) dotnet-maui: android: devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship + - 'Google Pixel 8-14.0' # Latest Android 14 + - 'Samsung Galaxy S23-13.0' # Popular flagship ios: devices: - - "iPhone 15-17.0" # Latest iOS 17 - - "iPhone 14-16.0" # iOS 16 support + - 'iPhone 15-17.0' # Latest iOS 17 + - 'iPhone 14-16.0' # iOS 16 support # Swift/iOS QuickStart swift: devices: - - "iPhone 15 Pro-17" # Latest flagship with iOS 17 + - 'iPhone 15 Pro-17' # Latest flagship with iOS 17 # Java Spring (web application) # Tests web app in desktop browsers using Selenium WebDriver java-spring: platforms: - - os: "Windows" - osVersion: "11" - browserName: "Chrome" - browserVersion: "latest" # Always test latest Chrome + - os: 'Windows' + osVersion: '11' + browserName: 'Chrome' + browserVersion: 'latest' # Always test latest Chrome # JavaScript Web (React web application) # Tests web app in multiple desktop browsers javascript-web: browsers: - - browser: "Chrome" - browser_version: "120.0" - os: "Windows" - os_version: "11" - - browser: "Firefox" - browser_version: "121.0" - os: "Windows" - os_version: "11" + - browser: 'Chrome' + browser_version: '120.0' + os: 'Windows' + os_version: '11' + - browser: 'Firefox' + browser_version: '121.0' + os: 'Windows' + os_version: '11' diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 000000000..681311eb9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/LICENSE b/LICENSE index 68d885abb..892015039 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright ©️ 2024 DittoLive, Inc. +Copyright ©️ 2026 DittoLive, Inc. 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: diff --git a/README.md b/README.md index dc5ab2c41..241f049ad 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ For support, please contact Ditto Support (). The Ditto SDK requires you to provide an identity for your application, which may be generated using the [Ditto Portal](https://portal.ditto.live/). For the purposes of these -quickstart applications, we'll be using the "Online Playground" identity type. +quickstart applications, we'll be using the "Development" identity type. ![Ditto Portal](.github/assets/ditto-portal.png) > [!IMPORTANT] -> The Online Playground identity type is _not_ suitable for production use. It is intended +> The Development identity type is _not_ suitable for production use. It is intended > only for development and testing purposes. To obtain your Ditto identity and configure the quickstart apps with it, follow these steps: @@ -27,7 +27,7 @@ To obtain your Ditto identity and configure the quickstart apps with it, follow 1. Copy the `.env.sample` file to `.env`. - in a terminal: `cp .env.sample .env`. - in a macOS Finder window, press `β‡§βŒ˜.` (SHIFT+CMD+period) to show hidden files. -1. Save your App ID, Online Playground Token, Auth URL, and WebSocket URL in the `.env` file. +1. Save your Database ID, Development Token, and Server URL in the `.env` file. Please see the app-specific README files for details on the tools necessary to build and run them. @@ -36,13 +36,16 @@ build and run them. - [Android Kotlin](android-kotlin/README.md) - [Android Java](android-java/README.md) +- [Kotlin Multiplatform](kotlin-multiplatform/README.md) - [Java Server](java-server/README.md) - [C++ TUI](cpp-tui/README.md) - [C# .NET MAUI](dotnet-maui/README.md) - [C# .NET TUI](dotnet-tui/README.md) - [C# .NET Win Forms](dotnet-winforms/README.md) +- [C# .NET Win Forms (.NET Framework 4.8)](dotnet-winforms-net48/README.md) - [Electron](electron/README.md) -- [Flutter](flutter_quickstart/README.md) +- [Flutter](flutter_app/README.md) +- [Go TUI](go-tui/README.md) - [Javascript TUI](javascript-tui/README.md) - [Javascript Web](javascript-web/README.md) - [React Native](react-native/README.md) diff --git a/android-java/README.md b/android-java/README.md index ad34bf0d7..c3b2cf278 100644 --- a/android-java/README.md +++ b/android-java/README.md @@ -13,7 +13,7 @@ After you have completed the [common prerequisites] you will need the following: ## Documentation - [Install Guide](https://docs.ditto.live/sdk/latest/install-guides/java/android) -- [API Reference](https://software.ditto.live/android/Ditto/5.0.0/api-reference/) +- [API Reference](https://docs.ditto.live/sdk/latest/api-reference/kotlin) - [SDK Release Notes](https://docs.ditto.live/sdk/latest/release-notes/java) [common prerequisites]: https://github.com/getditto/quickstart#common-prerequisites @@ -23,8 +23,8 @@ After you have completed the [common prerequisites] you will need the following: Assuming you have Android Studio and other prerequisites installed, you can build and run the app by following these steps: -1. Create an application at . Make note of the database ID (used to be called app ID) and online playground token. -2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID (used to be called AppId), Online Playground Token, and Auth URL. +1. Create an application at . Make note of the database ID and development token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID, Development Token, and Server URL. 3. Launch Android Studio and open the `quickstart/android-java` directory. 4. In Android Studio, select a connected Android device, or create and launch an Android emulator and select it as the destination, then choose the **Run > Run 'app'** menu item. @@ -50,12 +50,3 @@ to download the Ditto SDK from Maven Central and add it to the project: ```kotlin implementation(libs.ditto) ``` - -This line in `gradle/libs.versions.toml` specifies which version of the Ditto -SDK to use: - -```kotlin -ditto = "5.0.0" -``` - -To use a newer version of the SDK, change the version number on this line. diff --git a/android-java/app/build.gradle.kts b/android-java/app/build.gradle.kts index 11840d22c..1cf6ef128 100644 --- a/android-java/app/build.gradle.kts +++ b/android-java/app/build.gradle.kts @@ -16,9 +16,9 @@ fun loadEnvProperties(): Properties { FileInputStream(envFile).use { properties.load(it) } } else { val requiredEnvVars = listOf( - "DITTO_APP_ID", - "DITTO_PLAYGROUND_TOKEN", - "DITTO_AUTH_URL" + "DITTO_DATABASE_ID", + "DITTO_DEVELOPMENT_TOKEN", + "DITTO_SERVER_URL" ) for (envVar in requiredEnvVars) { @@ -30,8 +30,8 @@ fun loadEnvProperties(): Properties { return properties } -// Define BuildConfig.DITTO_APP_ID, BuildConfig.DITTO_PLAYGROUND_TOKEN, -// and BuildConfig.DITTO_AUTH_URL based on values in the .env file +// Define BuildConfig.DITTO_DATABASE_ID, BuildConfig.DITTO_DEVELOPMENT_TOKEN, +// and BuildConfig.DITTO_SERVER_URL based on values in the .env file // // More information can be found here: // https://docs.ditto.live/sdk/latest/install-guides/java/android#integrating-and-initializing @@ -43,28 +43,28 @@ androidComponents { onVariants { val prop = loadEnvProperties() it.buildConfigFields.put( - "DITTO_APP_ID", + "DITTO_DATABASE_ID", BuildConfigField( "String", - "\"${envValue(prop, "DITTO_APP_ID")}\"", - "Ditto application ID" + "\"${envValue(prop, "DITTO_DATABASE_ID")}\"", + "Ditto database ID" ) ) it.buildConfigFields.put( - "DITTO_PLAYGROUND_TOKEN", + "DITTO_DEVELOPMENT_TOKEN", BuildConfigField( "String", - "\"${envValue(prop, "DITTO_PLAYGROUND_TOKEN")}\"", - "Ditto online playground authentication token" + "\"${envValue(prop, "DITTO_DEVELOPMENT_TOKEN")}\"", + "Ditto development authentication token" ) ) it.buildConfigFields.put( - "DITTO_AUTH_URL", + "DITTO_SERVER_URL", BuildConfigField( "String", - "\"${envValue(prop, "DITTO_AUTH_URL")}\"", - "Ditto Auth URL" + "\"${envValue(prop, "DITTO_SERVER_URL")}\"", + "Ditto Server URL" ) ) } diff --git a/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt b/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt index bf3a6e3c2..28d68b722 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt +++ b/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt @@ -17,9 +17,9 @@ import java.util.function.Consumer object DittoHelper { @JvmStatic - fun createDitto(appId: String, serverUrl: String): Ditto { + fun createDitto(databaseId: String, serverUrl: String): Ditto { val config = DittoConfig( - databaseId = appId, + databaseId = databaseId, connect = DittoConfig.Connect.Server(serverUrl) ) return DittoFactory.create(config) diff --git a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java index 5df56efc9..49291da47 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java +++ b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java @@ -39,9 +39,9 @@ public class MainActivity extends ComponentActivity { DittoSyncSubscription taskSubscription; DittoStoreObserver taskObserver; - private final String dittoAppId = BuildConfig.DITTO_APP_ID; - private final String dittoPlaygroundToken = BuildConfig.DITTO_PLAYGROUND_TOKEN; - private final String dittoAuthUrl = BuildConfig.DITTO_AUTH_URL; + private final String dittoDatabaseId = BuildConfig.DITTO_DATABASE_ID; + private final String dittoDevelopmentToken = BuildConfig.DITTO_DEVELOPMENT_TOKEN; + private final String dittoServerUrl = BuildConfig.DITTO_SERVER_URL; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -57,15 +57,15 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { // Populate connection info (only in debug builds) if(BuildConfig.DEBUG) { - TextView appId = findViewById(R.id.ditto_app_id); - appId.setText(String.format("App ID: %s", dittoAppId)); + TextView databaseId = findViewById(R.id.ditto_app_id); + databaseId.setText(String.format("Database ID: %s", dittoDatabaseId)); - TextView playgroundToken = findViewById(R.id.ditto_playground_token); - playgroundToken.setText(String.format("Playground Token: %s", dittoPlaygroundToken)); + TextView developmentToken = findViewById(R.id.ditto_development_token); + developmentToken.setText(String.format("Development Token: %s", dittoDevelopmentToken)); } else { // Hide credential views in production findViewById(R.id.ditto_app_id).setVisibility(View.GONE); - findViewById(R.id.ditto_playground_token).setVisibility(View.GONE); + findViewById(R.id.ditto_development_token).setVisibility(View.GONE); } // Initialize "add task" fab @@ -98,9 +98,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { void initDitto() { Log.d("DittoInit", "=== Starting Ditto initialization ==="); - Log.d("DittoInit", "DITTO_APP_ID: " + dittoAppId); - Log.d("DittoInit", "DITTO_PLAYGROUND_TOKEN: " + (dittoPlaygroundToken != null ? "Present" : "NULL")); - Log.d("DittoInit", "DITTO_AUTH_URL: " + dittoAuthUrl); + Log.d("DittoInit", "DITTO_DATABASE_ID: " + dittoDatabaseId); + Log.d("DittoInit", "DITTO_DEVELOPMENT_TOKEN: " + (dittoDevelopmentToken != null ? "Present" : "NULL")); + Log.d("DittoInit", "DITTO_SERVER_URL: " + dittoServerUrl); // Skip permission requests during testing to avoid permission dialogs if (!isInstrumentationTest()) { @@ -115,12 +115,12 @@ void initDitto() { // Create Ditto with server connection // https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing Log.d("DittoInit", "Creating Ditto instance..."); - ditto = DittoHelper.createDitto(dittoAppId, dittoAuthUrl); + ditto = DittoHelper.createDitto(dittoDatabaseId, dittoServerUrl); Log.d("DittoInit", "Ditto instance created successfully"); // Set up authentication handler (must be set before sync.start()) Log.d("DittoInit", "Setting up authentication..."); - DittoHelper.setupAuth(ditto, dittoPlaygroundToken); + DittoHelper.setupAuth(ditto, dittoDevelopmentToken); Log.d("DittoInit", "Authentication configured"); // register subscription @@ -133,7 +133,7 @@ void initDitto() { // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers Log.d("DittoInit", "Registering observer..."); taskObserver = DittoHelper.registerObserver(ditto, - "SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC", + "SELECT * FROM tasks WHERE NOT deleted", result -> { Log.d("DittoInit", "Observer callback triggered with " + result.getItems().size() + " items"); var tasks = result.getItems().stream().map(Task::fromQueryItem).collect(Collectors.toCollection(ArrayList::new)); @@ -227,7 +227,7 @@ private void editTaskTitle(Task task, String newTitle) { try { // Update tasks into the ditto collection using DQL UPDATE statement // https://docs.ditto.live/sdk/latest/crud/update#updating - DittoHelper.execute(ditto, "UPDATE tasks SET title=:title WHERE _id=:id", args); + DittoHelper.execute(ditto, "UPDATE tasks SET title = :title WHERE _id = :id", args); } catch (Exception e) { e.printStackTrace(); } @@ -246,7 +246,7 @@ private void toggleTask(Task task) { try { // Update tasks into the ditto collection using DQL UPDATE statement // https://docs.ditto.live/sdk/latest/crud/update#updating - DittoHelper.execute(ditto, "UPDATE tasks SET done=:done WHERE _id=:id", args); + DittoHelper.execute(ditto, "UPDATE tasks SET done = :done WHERE _id = :id", args); } catch (Exception e) { e.printStackTrace(); } @@ -263,7 +263,7 @@ private void deleteTask(Task task) { try { // UPDATE DQL Statement using Soft-Delete pattern // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern - DittoHelper.execute(ditto, "UPDATE tasks SET deleted=true WHERE _id=:id", args); + DittoHelper.execute(ditto, "UPDATE tasks SET deleted = true WHERE _id = :id", args); } catch (Exception e) { e.printStackTrace(); } diff --git a/android-java/app/src/main/res/layout/activity_main.xml b/android-java/app/src/main/res/layout/activity_main.xml index ef89ef04c..e79c06aad 100644 --- a/android-java/app/src/main/res/layout/activity_main.xml +++ b/android-java/app/src/main/res/layout/activity_main.xml @@ -30,8 +30,8 @@ android:textColor="#757575" /> Add a new todo Add Todo Ditto Tasks - App ID - Playground Token + Database ID + Development Token \ No newline at end of file diff --git a/android-java/gradle/libs.versions.toml b/android-java/gradle/libs.versions.toml index ef0498e31..5e519c37f 100644 --- a/android-java/gradle/libs.versions.toml +++ b/android-java/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -ditto = "5.0.0" +ditto = "5.1.0" agp = "8.7.3" kotlin = "2.0.0" coreKtx = "1.10.1" diff --git a/android-kotlin/QuickStartTasks/app/build.gradle.kts b/android-kotlin/QuickStartTasks/app/build.gradle.kts index 6f7782f93..133ca506b 100644 --- a/android-kotlin/QuickStartTasks/app/build.gradle.kts +++ b/android-kotlin/QuickStartTasks/app/build.gradle.kts @@ -17,9 +17,9 @@ fun loadEnvProperties(): Properties { FileInputStream(envFile).use { properties.load(it) } } else { val requiredEnvVars = listOf( - "DITTO_APP_ID", - "DITTO_PLAYGROUND_TOKEN", - "DITTO_AUTH_URL" + "DITTO_DATABASE_ID", + "DITTO_DEVELOPMENT_TOKEN", + "DITTO_SERVER_URL" ) for (envVar in requiredEnvVars) { @@ -35,9 +35,9 @@ androidComponents { onVariants { val prop = loadEnvProperties() val buildConfigFields = mapOf( - "DITTO_APP_ID" to "Ditto application ID", - "DITTO_PLAYGROUND_TOKEN" to "Ditto playground token", - "DITTO_AUTH_URL" to "Ditto authentication URL", + "DITTO_DATABASE_ID" to "Ditto database ID", + "DITTO_DEVELOPMENT_TOKEN" to "Ditto development token", + "DITTO_SERVER_URL" to "Ditto server URL", "TEST_DOCUMENT_TITLE" to "Test document title for BrowserStack verification" ) diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt index 29393e39c..03f18980f 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt @@ -31,8 +31,8 @@ class TasksApplication : Application() { private fun initializeDitto() { try { val config = DittoConfig( - databaseId = BuildConfig.DITTO_APP_ID, - connect = DittoConfig.Connect.Server(url = BuildConfig.DITTO_AUTH_URL) + databaseId = BuildConfig.DITTO_DATABASE_ID, + connect = DittoConfig.Connect.Server(url = BuildConfig.DITTO_SERVER_URL) ) DittoHandler.initialize(config) @@ -45,7 +45,7 @@ class TasksApplication : Application() { private fun setupAuthentication() { try { - val token = BuildConfig.DITTO_PLAYGROUND_TOKEN + val token = BuildConfig.DITTO_DEVELOPMENT_TOKEN // Set the expiration handler before starting sync // https://docs.ditto.live/sdk/latest/sync/authentication diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt index c261d7bb9..7e4e24756 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt @@ -75,9 +75,9 @@ class EditScreenViewModel : ViewModel() { // Add tasks into the ditto collection using DQL INSERT statement // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents ditto.store.execute( - "INSERT INTO tasks DOCUMENTS (:doc)", + "INSERT INTO tasks DOCUMENTS (:task)", mapOf( - "doc" to mapOf( + "task" to mapOf( "title" to titleValue, "done" to doneValue, "deleted" to false diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt index 505fa3622..694f6d446 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt @@ -71,14 +71,14 @@ fun TasksListScreen(navController: NavController) { Text( text = stringResource( id = R.string.tasks_app_id, - BuildConfig.DITTO_APP_ID + BuildConfig.DITTO_DATABASE_ID ), style = MaterialTheme.typography.labelSmall ) Text( text = stringResource( id = R.string.tasks_token, - BuildConfig.DITTO_PLAYGROUND_TOKEN + BuildConfig.DITTO_DEVELOPMENT_TOKEN ), style = MaterialTheme.typography.labelSmall ) @@ -192,7 +192,7 @@ fun TasksListScreen(navController: NavController) { @Composable fun TasksList( tasks: List, - onToggle: ((taskId: String) -> Unit)? = null, + onToggle: ((task: Task) -> Unit)? = null, onClickEdit: ((taskId: String) -> Unit)? = null, onClickDelete: ((taskId: String) -> Unit)? = null, ) { @@ -200,7 +200,7 @@ fun TasksList( items(tasks, key = { it._id }) { task -> TaskRow( task = task, - onToggle = { onToggle?.invoke(it._id) }, + onToggle = { onToggle?.invoke(it) }, onClickEdit = { onClickEdit?.invoke(it._id) }, onClickDelete = { onClickDelete?.invoke(it._id) } ) diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt index 90787b247..ccf8e24e5 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt @@ -35,14 +35,19 @@ class TasksListScreenViewModel : ViewModel() { companion object { private const val TAG = "TasksListScreenViewModel" - private const val QUERY = "SELECT * FROM tasks WHERE NOT deleted" + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the displayed list. + private const val SUBSCRIPTION_QUERY = "SELECT * FROM tasks" + private const val OBSERVER_QUERY = + "SELECT * FROM tasks WHERE NOT deleted" } private val preferencesDataStore = TasksApplication.applicationContext().preferencesDataStore // Use StateFlow with store.observe() for reactive updates // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - val tasks: StateFlow> = ditto.store.observe(QUERY) { result -> + val tasks: StateFlow> = ditto.store.observe(OBSERVER_QUERY) { result -> result.items.map { item -> Task.fromJson(item.jsonString()) } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) @@ -55,8 +60,6 @@ class TasksListScreenViewModel : ViewModel() { private var syncSubscription: DittoSyncSubscription? = null init { - viewModelScope.launch { populateTasksCollection() } - // Apply the persisted sync preference whenever it changes. viewModelScope.launch { preferencesDataStore.data @@ -83,7 +86,7 @@ class TasksListScreenViewModel : ViewModel() { // Register a subscription, which determines what data syncs to this peer // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - syncSubscription = ditto.sync.registerSubscription(QUERY) + syncSubscription = ditto.sync.registerSubscription(SUBSCRIPTION_QUERY) } catch (e: Throwable) { Log.e(TAG, "Unable to start sync", e) } @@ -98,47 +101,15 @@ class TasksListScreenViewModel : ViewModel() { } } - // Add initial tasks to the collection if they have not already been added. - private suspend fun populateTasksCollection() { - val tasks = listOf( - Task("50191411-4C46-4940-8B72-5F8017A04FA7", "Buy groceries"), - Task("6DA283DA-8CFE-4526-A6FA-D385089364E5", "Clean the kitchen"), - Task("5303DDF8-0E72-4FEB-9E82-4B007E5797F0", "Schedule dentist appointment"), - Task("38411F1B-6B49-4346-90C3-0B16CE97E174", "Pay bills") - ) - - tasks.forEach { task -> + fun toggle(task: Task) { + viewModelScope.launch { try { - // Add tasks into the ditto collection using DQL INSERT statement - // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents + // Update tasks in the ditto collection using DQL UPDATE statement + // https://docs.ditto.live/sdk/latest/crud/update#updating ditto.store.execute( - "INSERT INTO tasks INITIAL DOCUMENTS (:task)", - mapOf("task" to task.toMap()) + "UPDATE tasks SET done = :done WHERE _id = :id", + mapOf("done" to !task.done, "id" to task._id) ) - } catch (e: Throwable) { - Log.e(TAG, "Unable to insert initial document", e) - } - } - } - - fun toggle(taskId: String) { - viewModelScope.launch { - try { - val task = ditto.store.execute( - "SELECT * FROM tasks WHERE _id = :_id AND NOT deleted", - mapOf("_id" to taskId) - ) { result -> - result.items.firstOrNull()?.let { Task.fromJson(it.jsonString()) } - } - - task?.let { - // Update tasks in the ditto collection using DQL UPDATE statement - // https://docs.ditto.live/sdk/latest/crud/update#updating - ditto.store.execute( - "UPDATE tasks SET done = :toggled WHERE _id = :_id AND NOT deleted", - mapOf("toggled" to !it.done, "_id" to taskId) - ) - } } catch (e: Throwable) { Log.e(TAG, "Unable to toggle done state", e) } diff --git a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml index e14417738..c8e7133c6 100644 --- a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml +++ b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml @@ -15,7 +15,7 @@ appcompat = "1.7.1" datastorePreferences = "1.2.1" koin-bom = "4.2.1" coroutines-tests = "1.10.2" -ditto = "5.0.0" +ditto = "5.1.0" monitor = "1.8.0" json = "20240303" diff --git a/android-kotlin/README.md b/android-kotlin/README.md index 515577b59..59ffb45fb 100644 --- a/android-kotlin/README.md +++ b/android-kotlin/README.md @@ -13,7 +13,7 @@ After you have completed the [common prerequisites] you will need the following: ## Documentation - [Kotlin Install Guide](https://docs.ditto.live/install-guides/kotlin) -- [Kotlin API Reference](https://software.ditto.live/android/Ditto/5.0.0/api-reference/) +- [Kotlin API Reference](https://docs.ditto.live/sdk/latest/api-reference/kotlin) - [Kotlin SDK Release Notes](https://docs.ditto.live/release-notes/kotlin) [common prerequisites]: https://github.com/getditto/quickstart#common-prerequisites @@ -23,8 +23,8 @@ After you have completed the [common prerequisites] you will need the following: Assuming you have Android Studio and other prerequisites installed, you can build and run the app by following these steps: -1. Create an application at . Make note of the database ID (used to be called app ID) and online playground token. -2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID (used to be called App ID), Online Playground Token, and Auth URL. +1. Create an application at . Make note of the database ID and development token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID, Development Token, and Server URL. 3. Launch Android Studio and open the `quickstart/android-kotlin` directory. 4. In Android Studio, select a connected Android device, or create and launch an Android emulator and select it as the destination, then choose the **Run > Run 'app'** menu item. @@ -55,17 +55,15 @@ Android Studio to automatically download the Ditto SDK from Maven Central and add it to the project: ```kotlin - implementation("live.ditto:ditto:4.11.1") + implementation(libs.com.ditto) ``` -To use a newer version of the SDK, change the version number in this line. - ### Initializing Ditto In `app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt`, you will see the `TasksApplication` class. This class is a subclass of `Application`. Its `onCreate()` method, which is called by the system when the app is launched, -calls `setupDitto()`, which gets application ID and online playground token from +calls `setupDitto()`, which gets Database ID and development token from the build configuration and uses it to create an instance of the `Ditto` class which is stored in a singleton object of the `DittoHandler` class. diff --git a/cpp-tui/README.md b/cpp-tui/README.md index b34c50949..7a2b6ef62 100644 --- a/cpp-tui/README.md +++ b/cpp-tui/README.md @@ -21,7 +21,7 @@ The build system will automatically download the Ditto C++ SDK when you run `mak ## Documentation - [C++ Install Guide](https://docs.ditto.live/install-guides/cpp) -- [C++ API Reference](https://software.ditto.live/cpp/Ditto/4.11.0/api-reference/) +- [C++ API Reference](https://docs.ditto.live/sdk/latest/api-reference/cpp) - [C++ Release Notes](https://docs.ditto.live/release-notes/cpp) [common prerequisites]: https://github.com/getditto/quickstart#common-prerequisites @@ -30,8 +30,8 @@ The build system will automatically download the Ditto C++ SDK when you run `mak Assuming you have the prerequisites installed, you can build and run the app by following these steps: -1. Create an application at . Make note of the app ID and online playground token. -2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your app ID and online playground token. +1. Create an application at . Make note of the Database ID and development token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID and development token. 3. In a shell, navigate to the `quickstart/cpp-tui/taskscpp` directory and run the command `make build` to automatically download the Ditto SDK and build the C++ application. The build system will detect your Linux architecture (x86_64/arm64) and download the appropriate Ditto C++ SDK version automatically. diff --git a/cpp-tui/taskscpp/Makefile b/cpp-tui/taskscpp/Makefile index fc2e95b7e..a34ddb72a 100644 --- a/cpp-tui/taskscpp/Makefile +++ b/cpp-tui/taskscpp/Makefile @@ -2,7 +2,7 @@ BUILD_TYPE ?= Debug # Ditto SDK version and platform detection -DITTO_SDK_VERSION ?= 4.14.3 +DITTO_SDK_VERSION ?= 5.1.0 PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]') ARCH := $(shell uname -m) diff --git a/cpp-tui/taskscpp/src/main.cpp b/cpp-tui/taskscpp/src/main.cpp index ac2dc4dea..e2daa179e 100644 --- a/cpp-tui/taskscpp/src/main.cpp +++ b/cpp-tui/taskscpp/src/main.cpp @@ -79,7 +79,6 @@ int main(int argc, const char *argv[]) { ("l,list", "List tasks") ("list-all", "List all tasks, including those marked deleted") ("m,monitor", "Monitor tasks for changes") - ("cleanup", "Evict all deleted tasks from local store") ("query", "Run a DQL query using the peer's Ditto instance", cxxopts::value>(), "STRING"); @@ -90,15 +89,12 @@ int main(int argc, const char *argv[]) { cxxopts::value()->default_value("5"), "N") ("p,persistence-directory", "Persistence directory", cxxopts::value(), "PATH") - ("app-id", "Ditto App ID", - cxxopts::value(), "APP_ID") - ("online-playground-token", "Ditto Online Playground token", + ("database-id", "Ditto Database ID", + cxxopts::value(), "DATABASE_ID") + ("development-token", "Ditto Development token", cxxopts::value(), "TOKEN") - ("websocket-url", "Ditto WebSocket URL", - cxxopts::value(), "WEBSOCKET_URL") - ("auth-url", "Ditto Auth URL", - cxxopts::value(), "AUTH_URL") - ("enable-cloud-sync", "Enable cloud synchronization"); + ("server-url", "Ditto Server URL", + cxxopts::value(), "SERVER_URL"); options.add_options("Logging") ("q,quiet", "Disable non-logging output") @@ -107,8 +103,6 @@ int main(int argc, const char *argv[]) { ("info", "Info-level logging") ("debug", "Debug-level logging") ("v,verbose","Trace-level logging") - ("log","Log file output path", - cxxopts::value(), "PATH") ("export", "Export-log file path", cxxopts::value(), "PATH") ("ditto-sdk-version", "Print the Ditto SDK version"); @@ -117,10 +111,10 @@ int main(int argc, const char *argv[]) { const auto opt_parse = options.parse(argc, argv); // If no other commands are specified, then "tui" is the default behavior. - const vector commands{"add", "complete", "incomplete", - "title", "delete", "list", - "list-all", "monitor", "cleanup", - "query", "toggle", "ditto-sdk-version"}; + const vector commands{"add", "complete", "incomplete", + "title", "delete", "list", + "list-all", "monitor", "query", + "toggle", "ditto-sdk-version"}; bool found_non_tui_command = false; for (const auto &command : commands) { if (opt_parse.count(command) > 0) { @@ -169,10 +163,6 @@ int main(int argc, const char *argv[]) { } set_minimum_log_level(log_level); - if (opt_parse.count("log") > 0) { - set_log_file(opt_parse["log"].as()); - } - if (opt_parse.count("export") > 0) { export_log_path = opt_parse["export"].as(); } @@ -184,21 +174,16 @@ int main(int argc, const char *argv[]) { opt_parse.count("persistence-directory") > 0 ? opt_parse["persistence-directory"].as() : ""; - const auto app_id = opt_parse.count("app-id") > 0 - ? opt_parse["app-id"].as() - : DITTO_APP_ID; - const auto online_playground_token = - opt_parse.count("online-playground-token") > 0 - ? opt_parse["online-playground-token"].as() - : DITTO_PLAYGROUND_TOKEN; - const auto websocket_url = opt_parse.count("websocket-url") > 0 - ? opt_parse["websocket-url"].as() - : DITTO_WEBSOCKET_URL; - const auto auth_url = opt_parse.count("auth-url") > 0 - ? opt_parse["auth-url"].as() - : DITTO_AUTH_URL; - - const auto enable_cloud_sync = opt_parse.count("enable-cloud-sync") > 0; + const auto database_id = opt_parse.count("database-id") > 0 + ? opt_parse["database-id"].as() + : DITTO_DATABASE_ID; + const auto development_token = + opt_parse.count("development-token") > 0 + ? opt_parse["development-token"].as() + : DITTO_DEVELOPMENT_TOKEN; + const auto server_url = opt_parse.count("server-url") > 0 + ? opt_parse["server-url"].as() + : DITTO_SERVER_URL; const auto quiet = opt_parse["quiet"].as(); @@ -207,9 +192,8 @@ int main(int argc, const char *argv[]) { // The peer is destroyed at the end of this scope { - TasksPeer peer(app_id, online_playground_token, websocket_url, auth_url, - enable_cloud_sync, persistence_dir); - peer.insert_initial_tasks(); + TasksPeer peer(database_id, development_token, server_url, + persistence_dir); peer.start_sync(); #ifdef DITTO_QUICKSTART_TUI @@ -383,19 +367,6 @@ int main(int argc, const char *argv[]) { } } - if (opt_parse.count("cleanup") > 0) { - need_post_sync = true; - try { - lock_guard lock(mtx); - peer.evict_deleted_tasks(); - if (!quiet) { - cout << "Evicted all deleted tasks" << endl; - } - } catch (const exception &err) { - cerr << "error: cleanup: " << err.what() << endl; - } - } - if (opt_parse.count("query") > 0) { need_post_sync = true; for (const auto &query : opt_parse["query"].as>()) { diff --git a/cpp-tui/taskscpp/src/tasks_log.cpp b/cpp-tui/taskscpp/src/tasks_log.cpp index 71da24384..74ac0823c 100644 --- a/cpp-tui/taskscpp/src/tasks_log.cpp +++ b/cpp-tui/taskscpp/src/tasks_log.cpp @@ -1,37 +1,33 @@ #include "tasks_log.h" -// These functions are all thin wrappers over the ditto::Log() API. Application -// code should call these rather than the ditto::Log() API to make it easy to -// change or extend the logging implementation. +// These functions are all thin wrappers over the ditto::Logger API. +// Application code should call these rather than the ditto::Logger API to make +// it easy to change or extend the logging implementation. -void log_error(const std::string &msg) { ditto::Log::e(msg); } +void log_error(const std::string &msg) { ditto::Logger::e(msg); } -void log_warning(const std::string &msg) { ditto::Log::w(msg); } +void log_warning(const std::string &msg) { ditto::Logger::w(msg); } -void log_info(const std::string &msg) { ditto::Log::i(msg); } +void log_info(const std::string &msg) { ditto::Logger::i(msg); } -void log_debug(const std::string &msg) { ditto::Log::d(msg); } +void log_debug(const std::string &msg) { ditto::Logger::d(msg); } -void log_verbose(const std::string &msg) { ditto::Log::v(msg); } +void log_verbose(const std::string &msg) { ditto::Logger::v(msg); } -bool get_logging_enabled() { return ditto::Log::get_logging_enabled(); } +bool get_logging_enabled() { return ditto::Logger::get_logging_enabled(); } void set_logging_enabled(bool enabled) { - ditto::Log::set_logging_enabled(enabled); + ditto::Logger::set_logging_enabled(enabled); } ditto::LogLevel get_minimum_log_level() { - return ditto::Log::get_minimum_log_level(); + return ditto::Logger::get_minimum_log_level(); } void set_minimum_log_level(ditto::LogLevel level) { - ditto::Log::set_minimum_log_level(level); + ditto::Logger::set_minimum_log_level(level); } -void set_log_file(const std::string &path) { ditto::Log::set_log_file(path); } - -void disable_log_file() { ditto::Log::disable_log_file(); } - void export_log(const std::string &path) { - ditto::Log::export_to_file(path).get(); + ditto::Logger::export_to_file(path).get(); } diff --git a/cpp-tui/taskscpp/src/tasks_log.h b/cpp-tui/taskscpp/src/tasks_log.h index 11d8b9616..1e0781851 100644 --- a/cpp-tui/taskscpp/src/tasks_log.h +++ b/cpp-tui/taskscpp/src/tasks_log.h @@ -17,9 +17,6 @@ void set_logging_enabled(bool enabled); ditto::LogLevel get_minimum_log_level(); void set_minimum_log_level(ditto::LogLevel level); -void set_log_file(const std::string &path); -void disable_log_file(); - void export_log(const std::string &path); #endif // DITTO_QUICKSTART_TASKS_LOG_H diff --git a/cpp-tui/taskscpp/src/tasks_peer.cpp b/cpp-tui/taskscpp/src/tasks_peer.cpp index c155c0591..b77399720 100644 --- a/cpp-tui/taskscpp/src/tasks_peer.cpp +++ b/cpp-tui/taskscpp/src/tasks_peer.cpp @@ -44,31 +44,46 @@ static string to_json_string(const ditto::QueryResult &result) { } /// Initialize a Ditto instance. -static shared_ptr -init_ditto(string app_id, string online_playground_token, string websocket_url, - string auth_url, bool enable_cloud_sync, string persistence_dir) { +static shared_ptr init_ditto(string database_id, + string development_token, + string server_url, + string persistence_dir) { try { - const auto identity = ditto::Identity::OnlinePlayground( - std::move(app_id), std::move(online_playground_token), - enable_cloud_sync, std::move(auth_url)); - - auto ditto = - std::make_shared(identity, std::move(persistence_dir)); - - ditto->update_transport_config( - [websocket_url](ditto::TransportConfig &config) { - config.enable_all_peer_to_peer(); - config.connect.websocket_urls.insert(websocket_url); - }); - - // Required for compatibility with DQL. - ditto->disable_sync_with_v3(); + auto config = ditto::DittoConfig::default_config() + .set_database_id(std::move(database_id)) + .set_persistence_directory(std::move(persistence_dir)) + .set_server_connect(std::move(server_url)); + + auto ditto = ditto::Ditto::open(std::move(config)); + + // Log in with the development token using the built-in development + // authentication provider. The expiration handler re-authenticates + // automatically when the credential is about to expire. + const auto provider = ditto::Authenticator::get_development_provider(); + if (auto auth = ditto->get_auth()) { + auth->set_expiration_handler( + [development_token, provider](ditto::Ditto &d, uint32_t) { + if (auto a = d.get_auth()) { + a->login(development_token, provider, + [](std::unique_ptr, + std::unique_ptr err) { + if (err) { + log_error("Failed to re-authenticate: " + + string(err->what())); + } + }); + } + }); - // Disable DQL strict mode - // https://docs.ditto.live/dql/strict-mode - const auto disableStrictModeCommand = - "ALTER SYSTEM SET DQL_STRICT_MODE = false"; - const auto result = ditto->get_store().execute(disableStrictModeCommand); + auth->login(development_token, provider, + [](std::unique_ptr, + std::unique_ptr err) { + if (err) { + log_error("Failed to authenticate: " + + string(err->what())); + } + }); + } return ditto; } catch (const exception &err) { @@ -85,21 +100,18 @@ class TasksPeer::Impl { // NOLINT(cppcoreguidelines-special-member-functions) string select_tasks_query(bool include_deleted_tasks = false) { if (include_deleted_tasks) { - return "SELECT * FROM tasks ORDER BY title ASC"; + return "SELECT * FROM tasks"; } else { - return "SELECT * FROM tasks WHERE NOT deleted ORDER BY title ASC"; + return "SELECT * FROM tasks WHERE NOT deleted"; } } public: - Impl(string app_id, string online_playground_token, string websocket_url, - string auth_url, bool enable_cloud_sync, string persistence_dir) + Impl(string database_id, string development_token, string server_url, + string persistence_dir) : mtx(new mutex()), - ditto(init_ditto(std::move(app_id), std::move(online_playground_token), - std::move(websocket_url), std::move(auth_url), - enable_cloud_sync, // This is required to be set to - // false to use the correct URLs - std::move(persistence_dir))) {} + ditto(init_ditto(std::move(database_id), std::move(development_token), + std::move(server_url), std::move(persistence_dir))) {} ~Impl() noexcept { try { @@ -116,7 +128,7 @@ class TasksPeer::Impl { // NOLINT(cppcoreguidelines-special-member-functions) return; } - ditto->start_sync(); + ditto->get_sync().start(); tasks_subscription = ditto->get_sync().register_subscription("SELECT * FROM tasks"); } @@ -128,18 +140,18 @@ class TasksPeer::Impl { // NOLINT(cppcoreguidelines-special-member-functions) tasks_subscription->cancel(); tasks_subscription.reset(); - ditto->stop_sync(); + ditto->get_sync().stop(); } - bool is_sync_active() const { return ditto->get_is_sync_active(); } + bool is_sync_active() const { return ditto->get_sync().is_active(); } string add_task(const string &title, bool done) { try { const json task_args = { {"title", title}, {"done", done}, {"deleted", false}}; - const auto command = "INSERT INTO tasks DOCUMENTS (:newTask)"; + const auto command = "INSERT INTO tasks DOCUMENTS (:task)"; const auto result = - ditto->get_store().execute(command, {{"newTask", task_args}}); + ditto->get_store().execute(command, {{"task", task_args}}); auto task_id = result.mutated_document_ids()[0]; log_debug("Added task: " + task_id.to_string()); return task_id.to_string(); @@ -303,20 +315,7 @@ class TasksPeer::Impl { // NOLINT(cppcoreguidelines-special-member-functions) log_debug("Deleted task: " + task_id); } catch (const exception &err) { log_error("Failed to delete task: " + string(err.what())); - throw runtime_error("unable to evict task: " + string(err.what())); - } - } - - void evict_deleted_tasks() { - try { - lock_guard lock(*mtx); - - const auto stmt = "EVICT FROM tasks WHERE deleted = true"; - ditto->get_store().execute(stmt); - log_debug("Evicted deleted tasks"); - } catch (const exception &err) { - log_error("Failed to evict deleted tasks: " + string(err.what())); - throw runtime_error("unable to evict task: " + string(err.what())); + throw runtime_error("unable to delete task: " + string(err.what())); } } @@ -324,8 +323,7 @@ class TasksPeer::Impl { // NOLINT(cppcoreguidelines-special-member-functions) std::function &)> callback) { try { const auto observer = ditto->get_store().register_observer( - select_tasks_query(), - [callback = std::move(callback)](const ditto::QueryResult &result) { + select_tasks_query(), [callback](const ditto::QueryResult &result) { const auto item_count = result.item_count(); log_debug("Tasks collection updated; count=" + to_string(item_count)); @@ -360,39 +358,12 @@ class TasksPeer::Impl { // NOLINT(cppcoreguidelines-special-member-functions) } } - void insert_initial_tasks() { - try { - lock_guard lock(*mtx); - - std::vector initial_tasks = { - {"50191411-4C46-4940-8B72-5F8017A04FA7", "Buy groceries"}, - {"6DA283DA-8CFE-4526-A6FA-D385089364E5", "Clean the kitchen"}, - {"5303DDF8-0E72-4FEB-9E82-4B007E5797F0", - "Schedule dentist appointment"}, - {"38411F1B-6B49-4346-90C3-0B16CE97E174", "Pay bills"}}; - - for (const auto &task : initial_tasks) { - const json task_args = {{"_id", task._id}, - {"title", task.title}, - {"done", task.done}, - {"deleted", task.deleted}}; - const auto command = "INSERT INTO tasks INITIAL DOCUMENTS (:newTask)"; - ditto->get_store().execute(command, {{"newTask", task_args}}); - } - } catch (const exception &err) { - log_error("Failed to insert initial tasks: " + string(err.what())); - throw runtime_error("unable to insert initial tasks: " + - string(err.what())); - } - } }; // class TasksPeer::Impl -TasksPeer::TasksPeer(string app_id, string online_playground_token, - string websocket_url, string auth_url, - bool enable_cloud_sync, string persistence_dir) - : impl(new Impl(std::move(app_id), std::move(online_playground_token), - std::move(websocket_url), std::move(auth_url), - enable_cloud_sync, std::move(persistence_dir))) {} +TasksPeer::TasksPeer(string database_id, string development_token, + string server_url, string persistence_dir) + : impl(new Impl(std::move(database_id), std::move(development_token), + std::move(server_url), std::move(persistence_dir))) {} TasksPeer::~TasksPeer() noexcept { try { @@ -437,8 +408,6 @@ void TasksPeer::delete_task(const string &task_id) { impl->delete_task(task_id); } -void TasksPeer::evict_deleted_tasks() { impl->evict_deleted_tasks(); } - shared_ptr TasksPeer::register_tasks_observer( function &)> callback) { return impl->register_tasks_observer(callback); @@ -449,7 +418,5 @@ string TasksPeer::execute_dql_query(const string &query) { } string TasksPeer::get_ditto_sdk_version() { - return ditto::Ditto::get_sdk_version(); + return ditto::Ditto::get_version(); } - -void TasksPeer::insert_initial_tasks() { impl->insert_initial_tasks(); } diff --git a/cpp-tui/taskscpp/src/tasks_peer.h b/cpp-tui/taskscpp/src/tasks_peer.h index 3a76307cf..ca2ae5e43 100644 --- a/cpp-tui/taskscpp/src/tasks_peer.h +++ b/cpp-tui/taskscpp/src/tasks_peer.h @@ -16,9 +16,8 @@ class TasksPeer { static std::string get_ditto_sdk_version(); /// Construct a new TasksPeer object. - TasksPeer(std::string ditto_app_id, std::string ditto_online_playground_token, - std::string ditto_websocket_url, std::string ditto_auth_url, - bool enable_cloud_sync, std::string ditto_persistence_dir); + TasksPeer(std::string database_id, std::string ditto_development_token, + std::string ditto_server_url, std::string ditto_persistence_dir); virtual ~TasksPeer() noexcept; @@ -80,14 +79,10 @@ class TasksPeer { /// Delete the specified task from the collection. /// - /// Note that this marks the task as deleted, and it will no longer appear in - /// `get_tasks()` results, but the object remains in the local store until - /// @ref `evict_deleted_tasks()` is called. + /// Note that this marks the task as deleted, so it will no longer appear in + /// `get_tasks()` results; the object remains in the local store. void delete_task(const std::string &task_id); - /// Remove all deleted tasks from the local store. - void evict_deleted_tasks(); - /// Run a DQL query using the peer's Ditto instance. /// /// This function is provided for diagnostic purposes. It should not be used @@ -105,9 +100,6 @@ class TasksPeer { std::shared_ptr register_tasks_observer( std::function &)> callback); - /// Add a set of initial documents to the tasks collection. - void insert_initial_tasks(); - private: class Impl; // private implementation class ("pimpl pattern") std::shared_ptr impl; diff --git a/cpp-tui/taskscpp/src/tasks_tui.cpp b/cpp-tui/taskscpp/src/tasks_tui.cpp index 753d03ec8..1ccfc0535 100644 --- a/cpp-tui/taskscpp/src/tasks_tui.cpp +++ b/cpp-tui/taskscpp/src/tasks_tui.cpp @@ -118,8 +118,8 @@ class TasksTui::Impl { : text("πŸ”΄ Sync Inactive") | color(Color::Red)) | bold, text(" (s: toggle sync)")}), - text("App ID: " DITTO_APP_ID) | center, - text("Playground Token: " DITTO_PLAYGROUND_TOKEN) | center, + text("Database ID: " DITTO_DATABASE_ID) | center, + text("Development Token: " DITTO_DEVELOPMENT_TOKEN) | center, }); }); auto bottom_bar = Renderer([this] { diff --git a/cpp-tui/taskscpp/tests/integration_test.cpp b/cpp-tui/taskscpp/tests/integration_test.cpp index a3610d570..b54c8f0b2 100644 --- a/cpp-tui/taskscpp/tests/integration_test.cpp +++ b/cpp-tui/taskscpp/tests/integration_test.cpp @@ -1,7 +1,7 @@ #include "../src/env.h" #include "../src/task.h" -#include "../src/tasks_peer.h" #include "../src/tasks_log.h" +#include "../src/tasks_peer.h" #include #include @@ -12,86 +12,92 @@ using std::cout; using std::endl; +using std::exception; using std::string; -using std::vector; using std::unique_ptr; -using std::exception; -using std::chrono::seconds; +using std::vector; +using std::chrono::duration_cast; using std::chrono::high_resolution_clock; using std::chrono::milliseconds; -using std::chrono::duration_cast; +using std::chrono::seconds; using std::this_thread::sleep_for; /** * Simple test that verifies GitHub-seeded document appears in synced task list */ int main() { - try { - cout << "C++ GitHub Seeded Document Test" << endl; - - // Get the exact document title that GitHub Actions seeded - const auto expected_title_env = getenv("DITTO_CLOUD_TASK_TITLE"); - if (!expected_title_env || string(expected_title_env).empty()) { - cout << "FAIL: Missing DITTO_CLOUD_TASK_TITLE environment variable" << endl; - std::exit(EXIT_FAILURE); - } + try { + cout << "C++ GitHub Seeded Document Test" << endl; + + // Get the exact document title that GitHub Actions seeded + const auto expected_title_env = getenv("DITTO_CLOUD_TASK_TITLE"); + if (!expected_title_env || string(expected_title_env).empty()) { + cout << "FAIL: Missing DITTO_CLOUD_TASK_TITLE environment variable" + << endl; + std::exit(EXIT_FAILURE); + } - string expected_title = string(expected_title_env); - cout << "Looking for seeded document: '" << expected_title << "'" << endl; - - // Initialize TasksPeer and start sync - cout << "Initializing Ditto sync..." << endl; - auto peer = unique_ptr(new TasksPeer( - DITTO_APP_ID, - DITTO_PLAYGROUND_TOKEN, - DITTO_WEBSOCKET_URL, - DITTO_AUTH_URL, - true, // enable_cloud_sync - "/tmp/cpp_integration_test" - )); - - peer->start_sync(); - cout << "Sync started, polling for document..." << endl; - - // Wait for sync and search for the exact document - const auto max_wait_seconds = 30; - const auto poll_interval_ms = 1000; - auto found = false; - - auto start_time = high_resolution_clock::now(); - - while (duration_cast(high_resolution_clock::now() - start_time).count() < max_wait_seconds && !found) { - auto elapsed = duration_cast(high_resolution_clock::now() - start_time).count(); - - vector tasks = peer->get_tasks(); - cout << "Checking " << tasks.size() << " synced tasks at " << elapsed << "s..." << endl; - - for (size_t i = 0; i < tasks.size(); i++) { - const auto& task = tasks[i]; - if (task.title == expected_title) { - cout << "SUCCESS: Found document '" << expected_title << "' at position " << i << endl; - found = true; - break; - } - } - - if (!found) { - sleep_for(milliseconds(poll_interval_ms)); - } + string expected_title = string(expected_title_env); + cout << "Looking for seeded document: '" << expected_title << "'" << endl; + + // Initialize TasksPeer and start sync + cout << "Initializing Ditto sync..." << endl; + auto peer = unique_ptr( + new TasksPeer(DITTO_DATABASE_ID, DITTO_DEVELOPMENT_TOKEN, + DITTO_SERVER_URL, "/tmp/cpp_integration_test")); + + peer->start_sync(); + cout << "Sync started, polling for document..." << endl; + + // Wait for sync and search for the exact document + const auto max_wait_seconds = 30; + const auto poll_interval_ms = 1000; + auto found = false; + + auto start_time = high_resolution_clock::now(); + + while (duration_cast(high_resolution_clock::now() - start_time) + .count() < max_wait_seconds && + !found) { + auto elapsed = + duration_cast(high_resolution_clock::now() - start_time) + .count(); + + vector tasks = peer->get_tasks(); + cout << "Checking " << tasks.size() << " synced tasks at " << elapsed + << "s..." << endl; + + for (size_t i = 0; i < tasks.size(); i++) { + const auto &task = tasks[i]; + if (task.title == expected_title) { + cout << "SUCCESS: Found document '" << expected_title + << "' at position " << i << endl; + found = true; + break; } + } - auto final_elapsed = duration_cast(high_resolution_clock::now() - start_time).count(); - - if (found) { - cout << "PASS: GitHub Actions β†’ Ditto Cloud β†’ C++ SDK sync verified in " << final_elapsed << "s" << endl; - return 0; - } else { - cout << "FAIL: Document '" << expected_title << "' not found after " << final_elapsed << "s" << endl; - std::exit(EXIT_FAILURE); - } + if (!found) { + sleep_for(milliseconds(poll_interval_ms)); + } + } - } catch (const exception& e) { - cout << "FAIL: Test exception: " << e.what() << endl; - std::exit(EXIT_FAILURE); + auto final_elapsed = + duration_cast(high_resolution_clock::now() - start_time) + .count(); + + if (found) { + cout << "PASS: GitHub Actions β†’ Ditto Cloud β†’ C++ SDK sync verified in " + << final_elapsed << "s" << endl; + return 0; + } else { + cout << "FAIL: Document '" << expected_title << "' not found after " + << final_elapsed << "s" << endl; + std::exit(EXIT_FAILURE); } + + } catch (const exception &e) { + cout << "FAIL: Test exception: " << e.what() << endl; + std::exit(EXIT_FAILURE); + } } \ No newline at end of file diff --git a/cpp-tui/taskscpp/tests/run_tests.sh b/cpp-tui/taskscpp/tests/run_tests.sh index 201251448..d9809264d 100755 --- a/cpp-tui/taskscpp/tests/run_tests.sh +++ b/cpp-tui/taskscpp/tests/run_tests.sh @@ -59,4 +59,4 @@ echo "" echo "πŸŽ‰ Integration Tests Completed Successfully!" echo "==========================================" echo "βœ… Integration tests: PASSED" -echo "🎯 C++ Ditto TUI application validated!" \ No newline at end of file +echo "🎯 C++ Ditto TUI application validated!" diff --git a/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj b/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj index 5619b566a..0358d3834 100644 --- a/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj +++ b/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj @@ -1,67 +1,67 @@ -ο»Ώ - - - net10.0-android;net10.0-ios;net10.0-maccatalyst - net10.0-windows10.0.19041.0;net10.0-android;net10.0-ios - net10.0-android - Exe - DittoMauiTasksApp - true - true - enable - - - DittoMauiTasksApp - - - live.ditto.quickstart.mauitasksapp - c8b2d96a-f86c-493c-9466-4a893592fbf9 - - 1.0 - 1 - - 15.0 - 24.0 - 15.0 - 10.0.17763.0 - - - - - QS Tasks - - - - - partial - true - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ο»Ώ + + + net10.0-android;net10.0-ios;net10.0-maccatalyst + net10.0-windows10.0.19041.0;net10.0-android;net10.0-ios + net10.0-android + Exe + DittoMauiTasksApp + true + true + enable + + + DittoMauiTasksApp + + + live.ditto.quickstart.mauitasksapp + c8b2d96a-f86c-493c-9466-4a893592fbf9 + + 1.0 + 1 + + 15.0 + 24.0 + 15.0 + 10.0.17763.0 + + + + + QS Tasks + + + + + partial + true + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs b/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs index e316382a6..405f1b795 100644 --- a/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs +++ b/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs @@ -1,131 +1,131 @@ -ο»Ώusing System.Reflection; - -using Microsoft.Extensions.Logging; - -using DittoMauiTasksApp.Utils; -using DittoMauiTasksApp.ViewModels; -using DittoSDK; -using DittoSDK.Auth; - -namespace DittoMauiTasksApp; - -public static class MauiProgram -{ - public static string AppId { get; private set; } = ""; - public static string PlaygroundToken { get; private set; } = ""; - - public static MauiApp CreateMauiApp() - { - var builder = MauiApp.CreateBuilder(); - builder - .UseMauiApp() - .ConfigureFonts(fonts => - { - fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); - fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); - }); - -#if DEBUG - builder.Logging.SetMinimumLevel(LogLevel.Debug); - builder.Logging.AddDebug(); -#endif - builder.Services.AddSingleton(SetupDitto()); - builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - - return builder.Build(); - } - - private static Ditto SetupDitto() - { - var envVars = LoadEnvVariables(); - AppId = envVars["DITTO_APP_ID"]; - PlaygroundToken = envVars["DITTO_PLAYGROUND_TOKEN"]; - var authUrl = envVars["DITTO_AUTH_URL"]; - - // New Initialization code - https://docs.ditto.live/sdk/latest/ditto-config - var dittoConfig = new DittoConfig( - AppId, - new DittoConfigConnect.Server( - new Uri(authUrl) +ο»Ώusing System.Reflection; + +using Microsoft.Extensions.Logging; + +using DittoMauiTasksApp.Utils; +using DittoMauiTasksApp.ViewModels; +using DittoSDK; +using DittoSDK.Auth; + +namespace DittoMauiTasksApp; + +public static class MauiProgram +{ + public static string DatabaseId { get; private set; } = ""; + public static string DevelopmentToken { get; private set; } = ""; + + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + +#if DEBUG + builder.Logging.SetMinimumLevel(LogLevel.Debug); + builder.Logging.AddDebug(); +#endif + builder.Services.AddSingleton(SetupDitto()); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + return builder.Build(); + } + + private static Ditto SetupDitto() + { + var envVars = LoadEnvVariables(); + DatabaseId = envVars["DITTO_DATABASE_ID"]; + DevelopmentToken = envVars["DITTO_DEVELOPMENT_TOKEN"]; + var serverUrl = envVars["DITTO_SERVER_URL"]; + + // New Initialization code - https://docs.ditto.live/sdk/latest/ditto-config + var dittoConfig = new DittoConfig( + DatabaseId, + new DittoConfigConnect.Server( + new Uri(serverUrl) ), - Path.Combine(FileSystem.Current.AppDataDirectory, "ditto") - ); - + Path.Combine(FileSystem.Current.AppDataDirectory, "ditto") + ); + var ditto = Ditto.Open(dittoConfig); // Set up authentication expiration handler (required for server connections) - ditto.Auth.ExpirationHandler = async (dittoAuth, secondsRemaining) => - { - // Authenticate when token is expiring - try - { + ditto.Auth.ExpirationHandler = async (dittoAuth, secondsRemaining) => + { + // Authenticate when token is expiring + try + { await dittoAuth.Auth.LoginAsync( // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider - DittoAuthenticationProvider.Development - ); - Console.WriteLine("Authentication successful"); - } - catch (Exception error) - { - Console.WriteLine($"Authentication failed: {error}"); - } + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider + DittoAuthenticationProvider.Development + ); + Console.WriteLine("Authentication successful"); + } + catch (Exception error) + { + Console.WriteLine($"Authentication failed: {error}"); + } }; - return ditto; - } - - /// - /// Load environment variables from the embedded .env resource file. - /// - static Dictionary LoadEnvVariables() - { - var envVars = new Dictionary(); - var assembly = Assembly.GetExecutingAssembly(); - string resourceName = "DittoMauiTasksApp..env"; - - using (Stream stream = assembly.GetManifestResourceStream(resourceName)) - { - if (stream == null) - { - var availableResources = string.Join(Environment.NewLine, assembly.GetManifestResourceNames()); - throw new InvalidOperationException($"Resource '{resourceName}' not found. Available resources: {availableResources}"); - } - - using (var reader = new StreamReader(stream)) - { - string line; - while ((line = reader.ReadLine()) != null) - { - line = line.Trim(); - - if (string.IsNullOrEmpty(line) || line.StartsWith("#")) - { - continue; - } - - int separatorIndex = line.IndexOf('='); - if (separatorIndex < 0) - { - continue; - } - - string key = line.Substring(0, separatorIndex).Trim(); - string value = line.Substring(separatorIndex + 1).Trim(); - - if (value.StartsWith("\"") && value.EndsWith("\"") && value.Length >= 2) - { - value = value.Substring(1, value.Length - 2); - } - - envVars[key] = value; - } - } - } - - return envVars; - } -} + return ditto; + } + + /// + /// Load environment variables from the embedded .env resource file. + /// + static Dictionary LoadEnvVariables() + { + var envVars = new Dictionary(); + var assembly = Assembly.GetExecutingAssembly(); + string resourceName = "DittoMauiTasksApp..env"; + + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + var availableResources = string.Join(Environment.NewLine, assembly.GetManifestResourceNames()); + throw new InvalidOperationException($"Resource '{resourceName}' not found. Available resources: {availableResources}"); + } + + using (var reader = new StreamReader(stream)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + line = line.Trim(); + + if (string.IsNullOrEmpty(line) || line.StartsWith("#")) + { + continue; + } + + int separatorIndex = line.IndexOf('='); + if (separatorIndex < 0) + { + continue; + } + + string key = line.Substring(0, separatorIndex).Trim(); + string value = line.Substring(separatorIndex + 1).Trim(); + + if (value.StartsWith("\"") && value.EndsWith("\"") && value.Length >= 2) + { + value = value.Substring(1, value.Length - 2); + } + + envVars[key] = value; + } + } + } + + return envVars; + } +} diff --git a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs index b2dfc7656..47b449a56 100644 --- a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs +++ b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs @@ -6,4 +6,4 @@ namespace DittoMauiTasksApp; public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); -} \ No newline at end of file +} diff --git a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs index c252a7fb9..544b0218f 100644 --- a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs +++ b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs @@ -12,4 +12,4 @@ static void Main(string[] args) // you can specify it here. UIApplication.Main(args, null, typeof(AppDelegate)); } -} \ No newline at end of file +} diff --git a/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs b/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs index f158f5b22..aa46f2e7a 100644 --- a/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs +++ b/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs @@ -1,14 +1,14 @@ -ο»Ώusing System; - -namespace DittoMauiTasksApp.Utils -{ - public class PopupService : IPopupService - { - public Task DisplayPromptAsync(string title, string message, string placeholder, string initialValue = "") - { - Page page = Application.Current?.Windows[0].Page; - return page.DisplayPromptAsync(title, message, placeholder: placeholder, initialValue: initialValue); - } - } -} - +ο»Ώusing System; + +namespace DittoMauiTasksApp.Utils +{ + public class PopupService : IPopupService + { + public Task DisplayPromptAsync(string title, string message, string placeholder, string initialValue = "") + { + Page page = Application.Current?.Windows[0].Page; + return page.DisplayPromptAsync(title, message, placeholder: placeholder, initialValue: initialValue); + } + } +} + diff --git a/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs b/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs index 066ff0113..aeb049fa1 100644 --- a/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs +++ b/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs @@ -1,370 +1,318 @@ -ο»Ώ -using System.Collections.ObjectModel; -using System.Text.Json; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using DittoMauiTasksApp.Utils; -using DittoSDK; -using DittoSDK.Sync; -using Microsoft.Extensions.Logging; - -namespace DittoMauiTasksApp.ViewModels -{ - public partial class TasksPageviewModel : ObservableObject - { - private const string SelectQuery = "SELECT * FROM tasks WHERE NOT deleted"; - - private readonly Ditto ditto; - private readonly IPopupService popupService; - private readonly ILogger logger; - private DittoSyncSubscription syncSubscription; - - public string AppIdText { get; } = $"App ID: {MauiProgram.AppId}"; - public string TokenText { get; } = $"Token: {MauiProgram.PlaygroundToken}"; - - [ObservableProperty] - ObservableCollection tasks; - - [ObservableProperty] - private bool isSyncEnabled = true; - - public TasksPageviewModel( - Ditto ditto, IPopupService popupService, ILogger logger) - { - this.ditto = ditto; - this.popupService = popupService; - this.logger = logger; -#if WINDOWS - try - { - Task.Run(async () => - { - await InsertInitialTasks(); - ObserveDittoTasksCollection(); - StartSync(); - - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); - } -#else - - DittoSyncPermissions.RequestPermissionsAsync().ContinueWith(async t => - { - try - { - await InsertInitialTasks(); - ObserveDittoTasksCollection(); - StartSync(); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); - } - }); -#endif - } - private async Task InsertInitialTasks() - { - try - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - var insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error adding initial tasks: {e.Message}"); - } - } - - [RelayCommand] - private async Task AddTaskAsync() - { - try - { - var title = await popupService.DisplayPromptAsync( - "Add Task", "Add a new task:", "Task title"); - - if (string.IsNullOrWhiteSpace(title)) - { - // nothing was entered - return; - } - title.Trim(); - - var doc = new Dictionary - { - {"title", title}, - {"done", false}, - {"deleted", false } - }; - var insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; - await ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "doc", doc } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error adding task: {e.Message}"); - } - } - - [RelayCommand] - private async Task EditTaskAsync(DittoTask task) - { - try - { - var newTitle = await popupService.DisplayPromptAsync( - "Edit Task", "Change task title:", "Task title", - initialValue: task.Title); - - if (string.IsNullOrWhiteSpace(newTitle)) - { - // nothing was entered - return; - } - newTitle.Trim(); - - var updateQuery = "UPDATE tasks " + - "SET title = :title " + - "WHERE _id = :id"; - await ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - {"title", newTitle}, - {"id", task.Id} - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error editing task: {e.Message}"); - } - } - - [RelayCommand] - private void DeleteTask(DittoTask task) - { - try - { - var updateQuery = "UPDATE tasks " + - "SET deleted = true " + - "WHERE _id = :id"; - ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - { "id", task.Id } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error deleting task: {e.Message}"); - } - } - - [RelayCommand] - private Task UpdateTaskDoneAsync(DittoTask task) - { - try - { - if (task == null) - { - logger.LogWarning("TasksPageviewModel: UpdateTaskDoneAsync called with null task"); - return Task.CompletedTask; - } - - var taskId = task.Id; - var newDoneState = task.Done; - - // Fire-and-forget the Ditto update to avoid blocking the UI - // thread while handling a checkbox change - _ = Task.Run(async () => - { - try - { - // Update the task done state only if it has changed, to - // avoid unnecessary calls to the store observer callback. - var updateQuery = "UPDATE tasks " + - "SET done = :newDoneState " + - "WHERE _id = :id AND done != :newDoneState"; - var result = await ditto.Store.ExecuteAsync(updateQuery, new Dictionary - { - { "newDoneState", newDoneState }, - { "id", taskId } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error updating task done state for {taskId}: {e.Message}"); - } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error updating task done state: {e.Message}"); - } - return Task.CompletedTask; - } - - private void ObserveDittoTasksCollection() - { - // Register observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - ditto.Store.RegisterObserver(SelectQuery, async (queryResult) => - { - try - { - var newTasks = queryResult.Items.Select(d => - JsonSerializer.Deserialize(d.JsonString()) - ).ToList(); - - MainThread.BeginInvokeOnMainThread(() => - { - try - { - if (Tasks == null) - { - Tasks = new ObservableCollection(newTasks); - } - else - { - UpdateTasks(newTasks); - } - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error: Unable to update list view model: {e.Message}"); - } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error: Unable to process tasks collection change: {e.Message}"); - } - }); - } - - private void UpdateTasks(List newTasks) - { - var oldCount = Tasks.Count; - var newCount = newTasks.Count; - var minCount = Math.Min(oldCount, newCount); - - for (var i = 0; i < minCount; i++) - { - var existingTask = Tasks[i]; - var newTask = newTasks[i]; - existingTask.Id = newTask.Id; - existingTask.Title = newTask.Title; - existingTask.Done = newTask.Done; - existingTask.Deleted = newTask.Deleted; - } - - if (oldCount < newCount) - { - for (var i = oldCount; i < newCount; i++) - { - Tasks.Add(newTasks[i]); - } - } - else if (oldCount > newCount) - { - for (var i = oldCount - 1; i >= newCount; i--) - { - Tasks.RemoveAt(i); - } - } - } - - partial void OnIsSyncEnabledChanged(bool value) - { - if (value) - { - StartSync(); - } - else - { - StopSync(); - } - } - - private void StartSync() - { - try - { - ditto.Sync.Start(); - - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - syncSubscription = ditto.Sync.RegisterSubscription(SelectQuery); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error starting Ditto sync: {e.Message}"); - } - } - - private void StopSync() - { - if (syncSubscription != null) - { - try - { - syncSubscription.Cancel(); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error cancelling sync subscription: {e.Message}"); - } - syncSubscription = null; - } - - try - { - ditto.Sync.Stop(); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error stopping Ditto sync: {e.Message}"); - } - } - } -} +ο»Ώ +using System.Collections.ObjectModel; +using System.Text.Json; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DittoMauiTasksApp.Utils; +using DittoSDK; +using DittoSDK.Sync; +using Microsoft.Extensions.Logging; + +namespace DittoMauiTasksApp.ViewModels +{ + public partial class TasksPageviewModel : ObservableObject + { + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; + + private readonly Ditto ditto; + private readonly IPopupService popupService; + private readonly ILogger logger; + private DittoSyncSubscription syncSubscription; + + public string DatabaseIdText { get; } = $"Database ID: {MauiProgram.DatabaseId}"; + public string TokenText { get; } = $"Token: {MauiProgram.DevelopmentToken}"; + + [ObservableProperty] + ObservableCollection tasks; + + [ObservableProperty] + private bool isSyncEnabled = true; + + public TasksPageviewModel( + Ditto ditto, IPopupService popupService, ILogger logger) + { + this.ditto = ditto; + this.popupService = popupService; + this.logger = logger; +#if WINDOWS + try + { + Task.Run(() => + { + ObserveDittoTasksCollection(); + StartSync(); + + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); + } +#else + + DittoSyncPermissions.RequestPermissionsAsync().ContinueWith(t => + { + try + { + ObserveDittoTasksCollection(); + StartSync(); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); + } + }); +#endif + } + + [RelayCommand] + private async Task AddTaskAsync() + { + try + { + var title = await popupService.DisplayPromptAsync( + "Add Task", "Add a new task:", "Task title"); + + if (string.IsNullOrWhiteSpace(title)) + { + // nothing was entered + return; + } + title.Trim(); + + var doc = new Dictionary + { + {"title", title}, + {"done", false}, + {"deleted", false } + }; + var insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; + await ditto.Store.ExecuteAsync(insertCommand, new Dictionary() + { + { "task", doc } + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error adding task: {e.Message}"); + } + } + + [RelayCommand] + private async Task EditTaskAsync(DittoTask task) + { + try + { + var newTitle = await popupService.DisplayPromptAsync( + "Edit Task", "Change task title:", "Task title", + initialValue: task.Title); + + if (string.IsNullOrWhiteSpace(newTitle)) + { + // nothing was entered + return; + } + newTitle.Trim(); + + var updateQuery = "UPDATE tasks " + + "SET title = :title " + + "WHERE _id = :id"; + await ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + {"title", newTitle}, + {"id", task.Id} + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error editing task: {e.Message}"); + } + } + + [RelayCommand] + private void DeleteTask(DittoTask task) + { + try + { + var updateQuery = "UPDATE tasks " + + "SET deleted = true " + + "WHERE _id = :id"; + ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + { "id", task.Id } + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error deleting task: {e.Message}"); + } + } + + [RelayCommand] + private Task UpdateTaskDoneAsync(DittoTask task) + { + try + { + if (task == null) + { + logger.LogWarning("TasksPageviewModel: UpdateTaskDoneAsync called with null task"); + return Task.CompletedTask; + } + + var taskId = task.Id; + var newDoneState = task.Done; + + // Fire-and-forget the Ditto update to avoid blocking the UI + // thread while handling a checkbox change + _ = Task.Run(async () => + { + try + { + var updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; + var result = await ditto.Store.ExecuteAsync(updateQuery, new Dictionary + { + { "done", newDoneState }, + { "id", taskId } + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error updating task done state for {taskId}: {e.Message}"); + } + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error updating task done state: {e.Message}"); + } + return Task.CompletedTask; + } + + private void ObserveDittoTasksCollection() + { + // Register observer, which runs against the local database on this peer + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => + { + try + { + var newTasks = queryResult.Items.Select(d => + JsonSerializer.Deserialize(d.JsonString()) + ).ToList(); + + MainThread.BeginInvokeOnMainThread(() => + { + try + { + if (Tasks == null) + { + Tasks = new ObservableCollection(newTasks); + } + else + { + UpdateTasks(newTasks); + } + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error: Unable to update list view model: {e.Message}"); + } + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error: Unable to process tasks collection change: {e.Message}"); + } + }); + } + + private void UpdateTasks(List newTasks) + { + var oldCount = Tasks.Count; + var newCount = newTasks.Count; + var minCount = Math.Min(oldCount, newCount); + + for (var i = 0; i < minCount; i++) + { + var existingTask = Tasks[i]; + var newTask = newTasks[i]; + existingTask.Id = newTask.Id; + existingTask.Title = newTask.Title; + existingTask.Done = newTask.Done; + existingTask.Deleted = newTask.Deleted; + } + + if (oldCount < newCount) + { + for (var i = oldCount; i < newCount; i++) + { + Tasks.Add(newTasks[i]); + } + } + else if (oldCount > newCount) + { + for (var i = oldCount - 1; i >= newCount; i--) + { + Tasks.RemoveAt(i); + } + } + } + + partial void OnIsSyncEnabledChanged(bool value) + { + if (value) + { + StartSync(); + } + else + { + StopSync(); + } + } + + private void StartSync() + { + try + { + ditto.Sync.Start(); + + // Register a subscription, which determines what data syncs to this peer + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + syncSubscription = ditto.Sync.RegisterSubscription(SubscriptionQuery); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error starting Ditto sync: {e.Message}"); + } + } + + private void StopSync() + { + if (syncSubscription != null) + { + try + { + syncSubscription.Cancel(); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error cancelling sync subscription: {e.Message}"); + } + syncSubscription = null; + } + + try + { + ditto.Sync.Stop(); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error stopping Ditto sync: {e.Message}"); + } + } + } +} diff --git a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml index c68c9bdce..951cbd4fc 100644 --- a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml +++ b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml @@ -1,93 +1,93 @@ -ο»Ώ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ο»Ώ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs index 32efb54f0..6ea861919 100644 --- a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs +++ b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs @@ -1,31 +1,31 @@ -ο»Ώusing DittoMauiTasksApp.ViewModels; - -namespace DittoMauiTasksApp; - -public partial class TasksPage : ContentPage -{ - public TasksPage(TasksPageviewModel viewModel) - { - InitializeComponent(); - - BindingContext = viewModel; - } - - // Event handler invoked when a checkbox on the page is checked or unchecked. - private void OnCheckBoxCheckedChanged(object sender, CheckedChangedEventArgs e) - { - if (sender is CheckBox checkBox && checkBox.BindingContext is DittoTask task) - { - if (!checkBox.IsVisible || !checkBox.IsEnabled) - { - return; - } - - var viewModel = BindingContext as TasksPageviewModel; - if (viewModel?.UpdateTaskDoneCommand.CanExecute(task) == true) - { - viewModel.UpdateTaskDoneCommand.Execute(task); - } - } - } -} +ο»Ώusing DittoMauiTasksApp.ViewModels; + +namespace DittoMauiTasksApp; + +public partial class TasksPage : ContentPage +{ + public TasksPage(TasksPageviewModel viewModel) + { + InitializeComponent(); + + BindingContext = viewModel; + } + + // Event handler invoked when a checkbox on the page is checked or unchecked. + private void OnCheckBoxCheckedChanged(object sender, CheckedChangedEventArgs e) + { + if (sender is CheckBox checkBox && checkBox.BindingContext is DittoTask task) + { + if (!checkBox.IsVisible || !checkBox.IsEnabled) + { + return; + } + + var viewModel = BindingContext as TasksPageviewModel; + if (viewModel?.UpdateTaskDoneCommand.CanExecute(task) == true) + { + viewModel.UpdateTaskDoneCommand.Execute(task); + } + } + } +} diff --git a/dotnet-maui/README.md b/dotnet-maui/README.md index 62a3e5885..cbd93495b 100644 --- a/dotnet-maui/README.md +++ b/dotnet-maui/README.md @@ -6,8 +6,8 @@ 2. Install the .NET MAUI workload by running `dotnet workload install maui`. 3. If you want to build and test the MAUI app for iOS, install Xcode from the Mac App Store. 4. If you want to build and test the MAUI app for Android, install Android Studio, or install the Android SDK, Java JDK, and Android emulator. -5. Create an application at . Make note of the app ID and online playground token -6. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your app ID and online playground token. +5. Create an application at . Make note of the Database ID and development token +6. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your Database ID and development token. ### Environment variables (Android builds and tests) @@ -16,7 +16,7 @@ The .NET Android SDK requires `JAVA_HOME` and (for the UI tests / Appium) `ANDRO ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.0/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ### Restore Packages ```sh diff --git a/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs b/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs index f8f6b886a..6eb6eaebe 100644 --- a/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs +++ b/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs @@ -1,86 +1,86 @@ -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.Android; -using UITests.Shared; - -namespace UITests.Android; - -public class AndroidTaskSearchTests : TaskSearchTests -{ - protected override AppiumDriver CreateDriver() - { - var options = new AppiumOptions(); - - // Check if running on BrowserStack - var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); - var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); - var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); - - if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) - { - // Load device config from environment (set by workflow from browserstack-devices.json) - var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); - if (string.IsNullOrEmpty(deviceString)) - { - throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); - } - var deviceParts = deviceString.Split('-'); - var deviceName = deviceParts[0]; - var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'Google Pixel 7-13.0')"); - - // BrowserStack capabilities - options.PlatformName = "Android"; - options.AutomationName = "UiAutomator2"; - options.DeviceName = deviceName; - options.PlatformVersion = platformVersion; - options.App = browserstackApp ?? GetAppPath(); - options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); - options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); - options.AddAdditionalAppiumOption("name", "Android Task Search Tests"); - - var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); - return new AndroidDriver(uri, options); - } - else - { - // Local testing capabilities - options.PlatformName = "Android"; - options.AutomationName = "UIAutomator2"; - options.App = GetAppPath(); - - // Optional capabilities for local testing - options.AddAdditionalAppiumOption("appWaitActivity", "crc647fcdc6dfabca042e.MainActivity"); - options.AddAdditionalAppiumOption("newCommandTimeout", 300); - options.AddAdditionalAppiumOption("autoGrantPermissions", true); - - // Optional: target a specific device when multiple are connected (e.g. APPIUM_UDID=emulator-5554) - var explicitUdid = Environment.GetEnvironmentVariable("APPIUM_UDID"); - if (!string.IsNullOrEmpty(explicitUdid)) - { - options.AddAdditionalAppiumOption("udid", explicitUdid); - } - - return new AndroidDriver(new Uri("http://127.0.0.1:4723"), options); - } - } - - private string GetAppPath() - { - var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); - var apkFileName = "live.ditto.quickstart.mauitasksapp-Signed.apk"; - - // Look for APK in release directory first (what we just built) - var releasePath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Release", "net10.0-android", apkFileName); - if (File.Exists(releasePath)) - { - return releasePath; - } - - var debugPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-android", apkFileName); - if (File.Exists(debugPath)) - { - return debugPath; - } - - throw new FileNotFoundException($"APK not found. Checked:\n- {debugPath}\n- {releasePath}"); - } -} \ No newline at end of file +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; +using UITests.Shared; + +namespace UITests.Android; + +public class AndroidTaskSearchTests : TaskSearchTests +{ + protected override AppiumDriver CreateDriver() + { + var options = new AppiumOptions(); + + // Check if running on BrowserStack + var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); + var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); + var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); + + if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) + { + // Load device config from environment (set by workflow from browserstack-devices.json) + var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); + if (string.IsNullOrEmpty(deviceString)) + { + throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); + } + var deviceParts = deviceString.Split('-'); + var deviceName = deviceParts[0]; + var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'Google Pixel 7-13.0')"); + + // BrowserStack capabilities + options.PlatformName = "Android"; + options.AutomationName = "UiAutomator2"; + options.DeviceName = deviceName; + options.PlatformVersion = platformVersion; + options.App = browserstackApp ?? GetAppPath(); + options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); + options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); + options.AddAdditionalAppiumOption("name", "Android Task Search Tests"); + + var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); + return new AndroidDriver(uri, options); + } + else + { + // Local testing capabilities + options.PlatformName = "Android"; + options.AutomationName = "UIAutomator2"; + options.App = GetAppPath(); + + // Optional capabilities for local testing + options.AddAdditionalAppiumOption("appWaitActivity", "crc647fcdc6dfabca042e.MainActivity"); + options.AddAdditionalAppiumOption("newCommandTimeout", 300); + options.AddAdditionalAppiumOption("autoGrantPermissions", true); + + // Optional: target a specific device when multiple are connected (e.g. APPIUM_UDID=emulator-5554) + var explicitUdid = Environment.GetEnvironmentVariable("APPIUM_UDID"); + if (!string.IsNullOrEmpty(explicitUdid)) + { + options.AddAdditionalAppiumOption("udid", explicitUdid); + } + + return new AndroidDriver(new Uri("http://127.0.0.1:4723"), options); + } + } + + private string GetAppPath() + { + var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); + var apkFileName = "live.ditto.quickstart.mauitasksapp-Signed.apk"; + + // Look for APK in release directory first (what we just built) + var releasePath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Release", "net10.0-android", apkFileName); + if (File.Exists(releasePath)) + { + return releasePath; + } + + var debugPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-android", apkFileName); + if (File.Exists(debugPath)) + { + return debugPath; + } + + throw new FileNotFoundException($"APK not found. Checked:\n- {debugPath}\n- {releasePath}"); + } +} diff --git a/dotnet-maui/UITests.Android/Program.cs b/dotnet-maui/UITests.Android/Program.cs index a70d634d8..a8419ba28 100644 --- a/dotnet-maui/UITests.Android/Program.cs +++ b/dotnet-maui/UITests.Android/Program.cs @@ -33,4 +33,4 @@ { // Ignore teardown errors } -} \ No newline at end of file +} diff --git a/dotnet-maui/UITests.Shared/BaseTest.cs b/dotnet-maui/UITests.Shared/BaseTest.cs index 5d9bf849d..98f1be580 100644 --- a/dotnet-maui/UITests.Shared/BaseTest.cs +++ b/dotnet-maui/UITests.Shared/BaseTest.cs @@ -1,123 +1,123 @@ -using NUnit.Framework; -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.Android; -using OpenQA.Selenium.Appium.iOS; -using System.Reflection; - -namespace UITests.Shared; - -public abstract class BaseTest -{ - protected AppiumDriver App { get; set; } = null!; - private static bool _assemblyResolveHooked = false; - - protected abstract AppiumDriver CreateDriver(); - - [OneTimeSetUp] - public void OneTimeSetUp() - { - if (!_assemblyResolveHooked) - { - AppDomain.CurrentDomain.AssemblyResolve += ResolveAppiumAssembly; - _assemblyResolveHooked = true; - } - } - - private static Assembly? ResolveAppiumAssembly(object? sender, ResolveEventArgs args) - { - var assemblyName = new AssemblyName(args.Name); - - if (assemblyName.Name == "Appium.Net") - { - var nugetPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".nuget", "packages", "appium.webdriver", "5.2.0", "lib", "net6.0", "Appium.Net.dll"); - - if (File.Exists(nugetPath)) - { - return Assembly.LoadFrom(nugetPath); - } - - var localPath = Path.Combine(AppContext.BaseDirectory, "Appium.Net.dll"); - if (File.Exists(localPath)) - { - return Assembly.LoadFrom(localPath); - } - } - - return null; - } - - [SetUp] - public void SetUp() - { - App = CreateDriver(); - - // Handle permission dialogs on app startup - HandlePermissionDialogs(); - } - - private void HandlePermissionDialogs() - { - try - { - Thread.Sleep(2000); - var isIOS = App.GetType().Name.Contains("IOSDriver"); - - if (isIOS) - { - var iOSButtons = new[] { "Allow", "OK", "Allow While Using App", "Allow Once" }; - - for (int dialogRound = 1; dialogRound <= 2; dialogRound++) - { - foreach (var buttonText in iOSButtons) - { - try - { - var allowButton = App.FindElement(MobileBy.XPath($"//XCUIElementTypeButton[@name='{buttonText}']")); - if (allowButton.Displayed) - { - allowButton.Click(); - Thread.Sleep(3000); - break; - } - } - catch - { - } - } - Thread.Sleep(1000); - } - } - else - { - var androidButtons = new[] { "Allow", "ALLOW", "OK", "Accept" }; - - foreach (var buttonText in androidButtons) - { - try - { - var allowButton = App.FindElement(MobileBy.XPath($"//android.widget.Button[@text='{buttonText}']")); - if (allowButton.Displayed) - { - allowButton.Click(); - Thread.Sleep(1000); - } - } - catch - { - } - } - } - } - catch - { - } - } - - [TearDown] - public void TearDown() - { - App?.Quit(); - } -} \ No newline at end of file +using NUnit.Framework; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; +using OpenQA.Selenium.Appium.iOS; +using System.Reflection; + +namespace UITests.Shared; + +public abstract class BaseTest +{ + protected AppiumDriver App { get; set; } = null!; + private static bool _assemblyResolveHooked = false; + + protected abstract AppiumDriver CreateDriver(); + + [OneTimeSetUp] + public void OneTimeSetUp() + { + if (!_assemblyResolveHooked) + { + AppDomain.CurrentDomain.AssemblyResolve += ResolveAppiumAssembly; + _assemblyResolveHooked = true; + } + } + + private static Assembly? ResolveAppiumAssembly(object? sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + + if (assemblyName.Name == "Appium.Net") + { + var nugetPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".nuget", "packages", "appium.webdriver", "5.2.0", "lib", "net6.0", "Appium.Net.dll"); + + if (File.Exists(nugetPath)) + { + return Assembly.LoadFrom(nugetPath); + } + + var localPath = Path.Combine(AppContext.BaseDirectory, "Appium.Net.dll"); + if (File.Exists(localPath)) + { + return Assembly.LoadFrom(localPath); + } + } + + return null; + } + + [SetUp] + public void SetUp() + { + App = CreateDriver(); + + // Handle permission dialogs on app startup + HandlePermissionDialogs(); + } + + private void HandlePermissionDialogs() + { + try + { + Thread.Sleep(2000); + var isIOS = App.GetType().Name.Contains("IOSDriver"); + + if (isIOS) + { + var iOSButtons = new[] { "Allow", "OK", "Allow While Using App", "Allow Once" }; + + for (int dialogRound = 1; dialogRound <= 2; dialogRound++) + { + foreach (var buttonText in iOSButtons) + { + try + { + var allowButton = App.FindElement(MobileBy.XPath($"//XCUIElementTypeButton[@name='{buttonText}']")); + if (allowButton.Displayed) + { + allowButton.Click(); + Thread.Sleep(3000); + break; + } + } + catch + { + } + } + Thread.Sleep(1000); + } + } + else + { + var androidButtons = new[] { "Allow", "ALLOW", "OK", "Accept" }; + + foreach (var buttonText in androidButtons) + { + try + { + var allowButton = App.FindElement(MobileBy.XPath($"//android.widget.Button[@text='{buttonText}']")); + if (allowButton.Displayed) + { + allowButton.Click(); + Thread.Sleep(1000); + } + } + catch + { + } + } + } + } + catch + { + } + } + + [TearDown] + public void TearDown() + { + App?.Quit(); + } +} diff --git a/dotnet-maui/UITests.Shared/TaskSearchTests.cs b/dotnet-maui/UITests.Shared/TaskSearchTests.cs index fb53c12d1..ebf095104 100644 --- a/dotnet-maui/UITests.Shared/TaskSearchTests.cs +++ b/dotnet-maui/UITests.Shared/TaskSearchTests.cs @@ -1,87 +1,87 @@ -using NUnit.Framework; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Support.UI; - -namespace UITests.Shared; - -[TestFixture] -public abstract class TaskSearchTests : BaseTest -{ - [Test] - public void CanFindTaskByTitle() - { - var expectedTaskTitle = Environment.GetEnvironmentVariable("DITTO_CLOUD_TASK_TITLE"); - - if (string.IsNullOrEmpty(expectedTaskTitle)) - { - Assert.Fail("DITTO_CLOUD_TASK_TITLE environment variable is not set or empty"); - return; - } - - var isBrowserStack = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME")); - - try - { - // Wait for app to load and sync - longer for BrowserStack - var waitTime = isBrowserStack ? 15000 : 6000; - Thread.Sleep(waitTime); - - var taskFound = false; - - for (int attempt = 1; attempt <= 10; attempt++) - { - try - { - // Find all TaskTitleLabel elements using AutomationId (works on both iOS and Android) - var taskLabels = App.FindElements(MobileBy.Id("TaskTitleLabel")); - - foreach (var label in taskLabels) - { - try - { - if (label.Text.Contains(expectedTaskTitle) && label.Displayed) - { - taskFound = true; - break; - } - } - catch - { - // Label not accessible, continue - } - } - - if (taskFound) - break; - } - catch - { - Thread.Sleep(1000); - } - } - - if (!taskFound) - { - if (isBrowserStack) - { - App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"failed\", \"reason\": \"Task not found\"}}"); - } - Assert.Fail($"Task '{expectedTaskTitle}' not found"); - } - - if (isBrowserStack) - { - App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"passed\", \"reason\": \"Task found successfully\"}}"); - } - } - catch (Exception ex) - { - if (isBrowserStack) - { - App.ExecuteScript($"browserstack_executor: {{\"action\": \"setSessionStatus\", \"arguments\": {{\"status\": \"failed\", \"reason\": \"{ex.Message}\"}}}}"); - } - Assert.Fail($"Test failed: {ex.Message}"); - } - } -} \ No newline at end of file +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Support.UI; + +namespace UITests.Shared; + +[TestFixture] +public abstract class TaskSearchTests : BaseTest +{ + [Test] + public void CanFindTaskByTitle() + { + var expectedTaskTitle = Environment.GetEnvironmentVariable("DITTO_CLOUD_TASK_TITLE"); + + if (string.IsNullOrEmpty(expectedTaskTitle)) + { + Assert.Fail("DITTO_CLOUD_TASK_TITLE environment variable is not set or empty"); + return; + } + + var isBrowserStack = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME")); + + try + { + // Wait for app to load and sync - longer for BrowserStack + var waitTime = isBrowserStack ? 15000 : 6000; + Thread.Sleep(waitTime); + + var taskFound = false; + + for (int attempt = 1; attempt <= 10; attempt++) + { + try + { + // Find all TaskTitleLabel elements using AutomationId (works on both iOS and Android) + var taskLabels = App.FindElements(MobileBy.Id("TaskTitleLabel")); + + foreach (var label in taskLabels) + { + try + { + if (label.Text.Contains(expectedTaskTitle) && label.Displayed) + { + taskFound = true; + break; + } + } + catch + { + // Label not accessible, continue + } + } + + if (taskFound) + break; + } + catch + { + Thread.Sleep(1000); + } + } + + if (!taskFound) + { + if (isBrowserStack) + { + App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"failed\", \"reason\": \"Task not found\"}}"); + } + Assert.Fail($"Task '{expectedTaskTitle}' not found"); + } + + if (isBrowserStack) + { + App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"passed\", \"reason\": \"Task found successfully\"}}"); + } + } + catch (Exception ex) + { + if (isBrowserStack) + { + App.ExecuteScript($"browserstack_executor: {{\"action\": \"setSessionStatus\", \"arguments\": {{\"status\": \"failed\", \"reason\": \"{ex.Message}\"}}}}"); + } + Assert.Fail($"Test failed: {ex.Message}"); + } + } +} diff --git a/dotnet-maui/UITests.iOS/Program.cs b/dotnet-maui/UITests.iOS/Program.cs index d6be342cd..9ada388c7 100644 --- a/dotnet-maui/UITests.iOS/Program.cs +++ b/dotnet-maui/UITests.iOS/Program.cs @@ -33,4 +33,4 @@ { // Ignore teardown errors } -} \ No newline at end of file +} diff --git a/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs b/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs index bc9a69e06..4a0d46ca5 100644 --- a/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs +++ b/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs @@ -1,127 +1,127 @@ -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.iOS; -using UITests.Shared; -using System.Diagnostics; -using System.Text.RegularExpressions; - -namespace UITests.iOS; - -public class iOSTaskSearchTests : TaskSearchTests -{ - protected override AppiumDriver CreateDriver() - { - var options = new AppiumOptions(); - - // Check if running on BrowserStack - var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); - var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); - var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); - - if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) - { - // Load device config from environment (set by workflow from browserstack-devices.json) - var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); - if (string.IsNullOrEmpty(deviceString)) - { - throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); - } - var deviceParts = deviceString.Split('-'); - var deviceName = deviceParts[0]; - var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'iPhone 15-17.0')"); - - // BrowserStack capabilities for iOS - options.PlatformName = "iOS"; - options.AutomationName = "XCUITest"; - options.DeviceName = deviceName; - options.PlatformVersion = platformVersion; - options.App = browserstackApp ?? GetAppPath(); - options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); - options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); - options.AddAdditionalAppiumOption("name", "iOS Task Search Tests"); - options.AddAdditionalAppiumOption("autoGrantPermissions", true); - - var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); - return new IOSDriver(uri, options); - } - else - { - // Local testing capabilities - options.PlatformName = "iOS"; - options.AutomationName = "XCUITest"; - - // App capabilities - use the built .app - options.App = Path.Combine(GetAppPath(), "DittoMauiTasksApp.app"); - - // iOS Simulator capabilities - automatically detect booted simulator - var (deviceName, platformVersion) = GetFirstBootedSimulator(); - options.DeviceName = deviceName; - options.PlatformVersion = platformVersion; - options.AddAdditionalAppiumOption("newCommandTimeout", 300); - return new IOSDriver(new Uri("http://127.0.0.1:4723"), options); - } - } - - private string GetAppPath() - { - // Look for the .app in the MAUI project's build output - var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); - var appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-arm64"); - - if (!Directory.Exists(appPath)) - { - // Fallback to x64 simulator - appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-x64"); - } - - return appPath; - } - - private static (string DeviceName, string PlatformVersion) GetFirstBootedSimulator() - { - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = "xcrun", - Arguments = "simctl list devices", - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - } - }; - - process.Start(); - var output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - // Parse output to find first booted device - var lines = output.Split('\n'); - string? currentRuntime = null; - - foreach (var line in lines) - { - // Check if line contains runtime version (e.g. "-- iOS 18.6 --") - var runtimeMatch = Regex.Match(line, @"-- iOS ([\d.]+) --"); - if (runtimeMatch.Success) - { - currentRuntime = runtimeMatch.Groups[1].Value; - continue; - } - - // Check if line contains a booted device - if (line.Contains("(Booted)") && currentRuntime != null) - { - // Extract device name (e.g. "iPhone 16 (8E5063C4-E64F-46BD-B445-1B22F85B3310) (Booted)") - var deviceMatch = Regex.Match(line, @"^\s+([^(]+)\s+\([^)]+\)\s+\(Booted\)"); - if (deviceMatch.Success) - { - var deviceName = deviceMatch.Groups[1].Value.Trim(); - return (deviceName, currentRuntime); - } - } - } - - // Fallback if no booted simulator found - throw new InvalidOperationException("No booted iOS simulators found. Please boot a simulator first."); - } -} \ No newline at end of file +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.iOS; +using UITests.Shared; +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace UITests.iOS; + +public class iOSTaskSearchTests : TaskSearchTests +{ + protected override AppiumDriver CreateDriver() + { + var options = new AppiumOptions(); + + // Check if running on BrowserStack + var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); + var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); + var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); + + if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) + { + // Load device config from environment (set by workflow from browserstack-devices.json) + var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); + if (string.IsNullOrEmpty(deviceString)) + { + throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); + } + var deviceParts = deviceString.Split('-'); + var deviceName = deviceParts[0]; + var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'iPhone 15-17.0')"); + + // BrowserStack capabilities for iOS + options.PlatformName = "iOS"; + options.AutomationName = "XCUITest"; + options.DeviceName = deviceName; + options.PlatformVersion = platformVersion; + options.App = browserstackApp ?? GetAppPath(); + options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); + options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); + options.AddAdditionalAppiumOption("name", "iOS Task Search Tests"); + options.AddAdditionalAppiumOption("autoGrantPermissions", true); + + var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); + return new IOSDriver(uri, options); + } + else + { + // Local testing capabilities + options.PlatformName = "iOS"; + options.AutomationName = "XCUITest"; + + // App capabilities - use the built .app + options.App = Path.Combine(GetAppPath(), "DittoMauiTasksApp.app"); + + // iOS Simulator capabilities - automatically detect booted simulator + var (deviceName, platformVersion) = GetFirstBootedSimulator(); + options.DeviceName = deviceName; + options.PlatformVersion = platformVersion; + options.AddAdditionalAppiumOption("newCommandTimeout", 300); + return new IOSDriver(new Uri("http://127.0.0.1:4723"), options); + } + } + + private string GetAppPath() + { + // Look for the .app in the MAUI project's build output + var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); + var appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-arm64"); + + if (!Directory.Exists(appPath)) + { + // Fallback to x64 simulator + appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-x64"); + } + + return appPath; + } + + private static (string DeviceName, string PlatformVersion) GetFirstBootedSimulator() + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "xcrun", + Arguments = "simctl list devices", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + // Parse output to find first booted device + var lines = output.Split('\n'); + string? currentRuntime = null; + + foreach (var line in lines) + { + // Check if line contains runtime version (e.g. "-- iOS 18.6 --") + var runtimeMatch = Regex.Match(line, @"-- iOS ([\d.]+) --"); + if (runtimeMatch.Success) + { + currentRuntime = runtimeMatch.Groups[1].Value; + continue; + } + + // Check if line contains a booted device + if (line.Contains("(Booted)") && currentRuntime != null) + { + // Extract device name (e.g. "iPhone 16 (8E5063C4-E64F-46BD-B445-1B22F85B3310) (Booted)") + var deviceMatch = Regex.Match(line, @"^\s+([^(]+)\s+\([^)]+\)\s+\(Booted\)"); + if (deviceMatch.Success) + { + var deviceName = deviceMatch.Groups[1].Value.Trim(); + return (deviceName, currentRuntime); + } + } + } + + // Fallback if no booted simulator found + throw new InvalidOperationException("No booted iOS simulators found. Please boot a simulator first."); + } +} diff --git a/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj b/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj index 9eb14b0ed..1ee65ceea 100644 --- a/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj +++ b/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs b/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs index d71fc65a4..11e45ad8c 100644 --- a/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs +++ b/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs @@ -18,14 +18,13 @@ public async Task TasksPeer_CanSyncAndRetrieveTasks() ?? throw new InvalidOperationException("DITTO_CLOUD_TASK_TITLE environment variable is required"); using var peer = await TasksPeer.Create( - env["DITTO_APP_ID"], - env["DITTO_PLAYGROUND_TOKEN"], - env["DITTO_AUTH_URL"], - env["DITTO_WEBSOCKET_URL"]); + env["DITTO_DATABASE_ID"], + env["DITTO_DEVELOPMENT_TOKEN"], + env["DITTO_SERVER_URL"]); // Verify connection Assert.NotNull(peer); - Assert.Equal(env["DITTO_APP_ID"], peer.AppId); + Assert.Equal(env["DITTO_DATABASE_ID"], peer.DatabaseId); var foundTask = false; var attempts = 0; @@ -89,4 +88,4 @@ private static Dictionary LoadEnvVariables() return envVars; } -} \ No newline at end of file +} diff --git a/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj b/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj index 6cef927a6..f6af8061d 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj +++ b/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj @@ -9,7 +9,7 @@ - + diff --git a/dotnet-tui/DittoDotNetTasksConsole/Program.cs b/dotnet-tui/DittoDotNetTasksConsole/Program.cs index 258fcd308..df40a77b0 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/Program.cs +++ b/dotnet-tui/DittoDotNetTasksConsole/Program.cs @@ -16,12 +16,11 @@ public static async Task Main(string[] args) try { var env = LoadEnvVariables(); - var appId = env["DITTO_APP_ID"]; - var playgroundToken = env["DITTO_PLAYGROUND_TOKEN"]; - var websocketUrl = env["DITTO_WEBSOCKET_URL"]; - var authUrl = env["DITTO_AUTH_URL"]; + var databaseId = env["DITTO_DATABASE_ID"]; + var developmentToken = env["DITTO_DEVELOPMENT_TOKEN"]; + var serverUrl = env["DITTO_SERVER_URL"]; - using var peer = await TasksPeer.Create(appId, playgroundToken, authUrl, websocketUrl); + using var peer = await TasksPeer.Create(databaseId, developmentToken, serverUrl); // Disable Ditto's standard-error logging, which would interfere // with the the Terminal.Gui UI. diff --git a/dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs b/dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs index 200fe4f4b..74ef8b75b 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs +++ b/dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs @@ -14,12 +14,15 @@ /// public class TasksPeer : IDisposable { - private const string Query = "SELECT * FROM tasks WHERE NOT deleted"; + // The subscription controls what syncs to this device and is written to the + // local database; the observer reacts to changes in that local database, + // hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; - public string AppId { get; private set; } - public string PlaygroundToken { get; private set; } - public string AuthUrl { get; private set; } - public string WebsocketUrl { get; private set; } + public string DatabaseId { get; private set; } + public string DevelopmentToken { get; private set; } + public string ServerUrl { get; private set; } public bool IsSyncActive => _ditto.Sync.IsActive; @@ -28,19 +31,17 @@ public class TasksPeer : IDisposable /// /// Creates a new synchronizing TasksPeer instance. /// - public static async Task Create( - string appId, - string playgroundToken, - string authUrl, - string websocketUrl) + public static Task Create( + string databaseId, + string developmentToken, + string serverUrl) { - var peer = new TasksPeer(appId, playgroundToken, authUrl, websocketUrl); + var peer = new TasksPeer(databaseId, developmentToken, serverUrl); peer.Authenticate(); peer.RegisterSubscription(); - await peer.InsertInitialTasks(); peer.StartSync(); - return peer; + return Task.FromResult(peer); } private void Authenticate() @@ -52,8 +53,8 @@ private void Authenticate() { await ditto.Auth.LoginAsync( // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider DittoAuthenticationProvider.Development ); } @@ -70,9 +71,9 @@ await ditto.Auth.LoginAsync( /// will be synced to this peer during the synchronization process. /// /// - /// The subscription is created using the same query that filters out deleted tasks - /// (SELECT * FROM tasks WHERE NOT deleted), ensuring that only active, - /// non-deleted tasks are synchronized across the network. + /// The subscription syncs all tasks (SELECT * FROM tasks) and controls + /// what is written to this peer's local database; the observer query hides + /// soft-deleted tasks from the displayed list. /// /// This method should be called during peer initialization to establish the /// subscription before starting the sync process. The subscription remains active @@ -83,26 +84,24 @@ private void RegisterSubscription() { // Register a subscription, which determines what data syncs to this peer // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - _ditto.Sync.RegisterSubscription(Query); + _ditto.Sync.RegisterSubscription(SubscriptionQuery); } /// /// Constructor /// - /// Ditto application ID - /// Ditto online playground token - /// Ditto Auth URL - /// Ditto Websocket URL - private TasksPeer(string appId, string playgroundToken, string authUrl, string websocketUrl) + /// Ditto database ID + /// Ditto development token + /// Ditto Server URL + private TasksPeer(string databaseId, string developmentToken, string serverUrl) { - AppId = appId; - PlaygroundToken = playgroundToken; - AuthUrl = authUrl; - WebsocketUrl = websocketUrl; + DatabaseId = databaseId; + DevelopmentToken = developmentToken; + ServerUrl = serverUrl; var config = new DittoConfig( - AppId, - new DittoConfigConnect.Server(new Uri(authUrl)) + DatabaseId, + new DittoConfigConnect.Server(new Uri(serverUrl)) ); _ditto = Ditto.Open(config); @@ -114,53 +113,6 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Inserts the initial tasks into the 'tasks' collection. - /// - private async Task InsertInitialTasks() - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - const string insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - /// /// Adds a new task to the 'tasks' collection. /// @@ -177,10 +129,10 @@ public async Task AddTask(string title) {"done", false}, {"deleted", false } }; - const string insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; + const string insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() { - { "doc", doc } + { "task", doc } }); } @@ -229,10 +181,10 @@ public async Task DeleteTask(string taskId) /// public async Task UpdateTaskDone(string taskId, bool newDoneState) { - const string updateQuery = "UPDATE tasks SET done = :newDoneState WHERE _id = :id AND done != :newDoneState"; + const string updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary { - { "newDoneState", newDoneState }, + { "done", newDoneState }, { "id", taskId } }); } @@ -244,7 +196,7 @@ public DittoStoreObserver ObserveTasksCollection(Func, Task> han { // Register an observer, which runs against the local database on this peer // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - return _ditto.Store.RegisterObserver(Query, async (queryResult) => + return _ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => { try { @@ -281,4 +233,4 @@ public void StopSync() } _ditto.Sync.Stop(); } -} \ No newline at end of file +} diff --git a/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs b/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs index 79b431d28..aff2fcd95 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs +++ b/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs @@ -165,13 +165,13 @@ public TasksWindow(TasksPeer peer) : base($"Ditto Tasks") }; Add(syncStatusLabel); - Add(new Label("App ID: " + _peer.AppId) + Add(new Label("Database ID: " + _peer.DatabaseId) { X = Pos.Center(), Y = 1, }); - Add(new Label("Playground Token: " + _peer.PlaygroundToken) + Add(new Label("Development Token: " + _peer.DevelopmentToken) { X = Pos.Center(), Y = 2, diff --git a/dotnet-tui/README.md b/dotnet-tui/README.md index bf36e7a40..bfe31cdd5 100644 --- a/dotnet-tui/README.md +++ b/dotnet-tui/README.md @@ -4,14 +4,14 @@ ## Prerequisites 1. Install the .NET 10 SDK from -2. Create an application at . Make note of the app ID and online playground token -3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your app ID and online playground token. +2. Create an application at . Make note of the Database ID and development token +3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your Database ID and development token. ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.0/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ## .NET Console App diff --git a/dotnet-winforms-net48/.env.sample b/dotnet-winforms-net48/.env.sample index a05d0fcf6..4740190e2 100644 --- a/dotnet-winforms-net48/.env.sample +++ b/dotnet-winforms-net48/.env.sample @@ -1,7 +1,7 @@ -#!/usr/bin/env bash - -# Copy this file from ".env.sample" to ".env", then fill in these values -# A Ditto AppID and Playground token can be obtained from https://portal.ditto.live -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" \ No newline at end of file +#!/usr/bin/env bash + +# Copy this file from ".env.sample" to ".env", then fill in these values +# A Ditto Database ID and development token can be obtained from https://portal.ditto.live +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" \ No newline at end of file diff --git a/dotnet-winforms-net48/App.config b/dotnet-winforms-net48/App.config index fcde7c8d9..533e4c0c2 100644 --- a/dotnet-winforms-net48/App.config +++ b/dotnet-winforms-net48/App.config @@ -1,18 +1,18 @@ -ο»Ώ - - - - - - - - - - - - - - - - +ο»Ώ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet-winforms-net48/AppConfiguration.cs b/dotnet-winforms-net48/AppConfiguration.cs index 4fa69131f..75de2ffba 100644 --- a/dotnet-winforms-net48/AppConfiguration.cs +++ b/dotnet-winforms-net48/AppConfiguration.cs @@ -9,9 +9,9 @@ namespace Taskapp.WinForms.Net48 /// public static class AppConfiguration { - public static string AppId { get; private set; } - public static string PlaygroundToken { get; private set; } - public static string AuthUrl { get; private set; } + public static string DatabaseId { get; private set; } + public static string DevelopmentToken { get; private set; } + public static string ServerUrl { get; private set; } /// /// Loads configuration from .env file in the application directory @@ -42,26 +42,26 @@ public static void Load() switch (key) { - case "DITTO_APP_ID": - AppId = value; + case "DITTO_DATABASE_ID": + DatabaseId = value; break; - case "DITTO_PLAYGROUND_TOKEN": - PlaygroundToken = value; + case "DITTO_DEVELOPMENT_TOKEN": + DevelopmentToken = value; break; - case "DITTO_AUTH_URL": - AuthUrl = value; + case "DITTO_SERVER_URL": + ServerUrl = value; break; } } } // Validate required fields - if (string.IsNullOrWhiteSpace(AppId)) - throw new InvalidOperationException("DITTO_APP_ID is required in .env file"); - if (string.IsNullOrWhiteSpace(PlaygroundToken)) - throw new InvalidOperationException("DITTO_PLAYGROUND_TOKEN is required in .env file"); - if (string.IsNullOrWhiteSpace(AuthUrl)) - throw new InvalidOperationException("DITTO_AUTH_URL is required in .env file"); + if (string.IsNullOrWhiteSpace(DatabaseId)) + throw new InvalidOperationException("DITTO_DATABASE_ID is required in .env file"); + if (string.IsNullOrWhiteSpace(DevelopmentToken)) + throw new InvalidOperationException("DITTO_DEVELOPMENT_TOKEN is required in .env file"); + if (string.IsNullOrWhiteSpace(ServerUrl)) + throw new InvalidOperationException("DITTO_SERVER_URL is required in .env file"); } } } diff --git a/dotnet-winforms-net48/MainForm.Designer.cs b/dotnet-winforms-net48/MainForm.Designer.cs index 241f35ba8..98f66fab3 100644 --- a/dotnet-winforms-net48/MainForm.Designer.cs +++ b/dotnet-winforms-net48/MainForm.Designer.cs @@ -1,164 +1,164 @@ -ο»Ώnamespace Taskapp.WinForms.Net48 -{ - partial class MainForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.tasksListView = new System.Windows.Forms.ListView(); - this.doneColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.titleColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.idColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.newTaskTextBox = new System.Windows.Forms.TextBox(); - this.addTaskButton = new System.Windows.Forms.Button(); - this.editTaskButton = new System.Windows.Forms.Button(); - this.deleteTaskButton = new System.Windows.Forms.Button(); - this.statusLabel = new System.Windows.Forms.Label(); - this.SuspendLayout(); - // - // tasksListView - // - this.tasksListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.tasksListView.CheckBoxes = true; - this.tasksListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { - this.doneColumn, - this.titleColumn, - this.idColumn}); - this.tasksListView.FullRowSelect = true; - this.tasksListView.GridLines = true; - this.tasksListView.HideSelection = false; - this.tasksListView.Location = new System.Drawing.Point(12, 12); - this.tasksListView.Name = "tasksListView"; - this.tasksListView.Size = new System.Drawing.Size(760, 350); - this.tasksListView.TabIndex = 0; - this.tasksListView.UseCompatibleStateImageBehavior = false; - this.tasksListView.View = System.Windows.Forms.View.Details; - this.tasksListView.ItemChecked += new System.Windows.Forms.ItemCheckedEventHandler(this.tasksListView_ItemChecked); - // - // doneColumn - // - this.doneColumn.Text = "Done"; - this.doneColumn.Width = 50; - // - // titleColumn - // - this.titleColumn.Text = "Title"; - this.titleColumn.Width = 500; - // - // idColumn - // - this.idColumn.Text = "ID"; - this.idColumn.Width = 0; - // - // newTaskTextBox - // - this.newTaskTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.newTaskTextBox.Location = new System.Drawing.Point(12, 378); - this.newTaskTextBox.Name = "newTaskTextBox"; - this.newTaskTextBox.Size = new System.Drawing.Size(540, 20); - this.newTaskTextBox.TabIndex = 1; - this.newTaskTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.newTaskTextBox_KeyPress); - // - // addTaskButton - // - this.addTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.addTaskButton.Location = new System.Drawing.Point(558, 376); - this.addTaskButton.Name = "addTaskButton"; - this.addTaskButton.Size = new System.Drawing.Size(100, 23); - this.addTaskButton.TabIndex = 2; - this.addTaskButton.Text = "Add Task"; - this.addTaskButton.UseVisualStyleBackColor = true; - this.addTaskButton.Click += new System.EventHandler(this.addTaskButton_Click); - // - // editTaskButton - // - this.editTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.editTaskButton.Location = new System.Drawing.Point(664, 376); - this.editTaskButton.Name = "editTaskButton"; - this.editTaskButton.Size = new System.Drawing.Size(100, 23); - this.editTaskButton.TabIndex = 3; - this.editTaskButton.Text = "Edit Task"; - this.editTaskButton.UseVisualStyleBackColor = true; - this.editTaskButton.Click += new System.EventHandler(this.editTaskButton_Click); - // - // deleteTaskButton - // - this.deleteTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.deleteTaskButton.Location = new System.Drawing.Point(664, 405); - this.deleteTaskButton.Name = "deleteTaskButton"; - this.deleteTaskButton.Size = new System.Drawing.Size(100, 23); - this.deleteTaskButton.TabIndex = 4; - this.deleteTaskButton.Text = "Delete Task"; - this.deleteTaskButton.UseVisualStyleBackColor = true; - this.deleteTaskButton.Click += new System.EventHandler(this.deleteTaskButton_Click); - // - // statusLabel - // - this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.statusLabel.AutoSize = true; - this.statusLabel.Location = new System.Drawing.Point(12, 410); - this.statusLabel.Name = "statusLabel"; - this.statusLabel.Size = new System.Drawing.Size(79, 13); - this.statusLabel.TabIndex = 5; - this.statusLabel.Text = "Status: Ready"; - // - // MainForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(784, 441); - this.Controls.Add(this.statusLabel); - this.Controls.Add(this.deleteTaskButton); - this.Controls.Add(this.editTaskButton); - this.Controls.Add(this.addTaskButton); - this.Controls.Add(this.newTaskTextBox); - this.Controls.Add(this.tasksListView); - this.Name = "MainForm"; - this.Text = "Tasks"; - this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); - this.Load += new System.EventHandler(this.MainForm_Load); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.ListView tasksListView; - private System.Windows.Forms.ColumnHeader doneColumn; - private System.Windows.Forms.ColumnHeader titleColumn; - private System.Windows.Forms.ColumnHeader idColumn; - private System.Windows.Forms.TextBox newTaskTextBox; - private System.Windows.Forms.Button addTaskButton; - private System.Windows.Forms.Button editTaskButton; - private System.Windows.Forms.Button deleteTaskButton; - private System.Windows.Forms.Label statusLabel; - } -} - +ο»Ώnamespace Taskapp.WinForms.Net48 +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tasksListView = new System.Windows.Forms.ListView(); + this.doneColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.titleColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.idColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.newTaskTextBox = new System.Windows.Forms.TextBox(); + this.addTaskButton = new System.Windows.Forms.Button(); + this.editTaskButton = new System.Windows.Forms.Button(); + this.deleteTaskButton = new System.Windows.Forms.Button(); + this.statusLabel = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // tasksListView + // + this.tasksListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.tasksListView.CheckBoxes = true; + this.tasksListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.doneColumn, + this.titleColumn, + this.idColumn}); + this.tasksListView.FullRowSelect = true; + this.tasksListView.GridLines = true; + this.tasksListView.HideSelection = false; + this.tasksListView.Location = new System.Drawing.Point(12, 12); + this.tasksListView.Name = "tasksListView"; + this.tasksListView.Size = new System.Drawing.Size(760, 350); + this.tasksListView.TabIndex = 0; + this.tasksListView.UseCompatibleStateImageBehavior = false; + this.tasksListView.View = System.Windows.Forms.View.Details; + this.tasksListView.ItemChecked += new System.Windows.Forms.ItemCheckedEventHandler(this.tasksListView_ItemChecked); + // + // doneColumn + // + this.doneColumn.Text = "Done"; + this.doneColumn.Width = 50; + // + // titleColumn + // + this.titleColumn.Text = "Title"; + this.titleColumn.Width = 500; + // + // idColumn + // + this.idColumn.Text = "ID"; + this.idColumn.Width = 0; + // + // newTaskTextBox + // + this.newTaskTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.newTaskTextBox.Location = new System.Drawing.Point(12, 378); + this.newTaskTextBox.Name = "newTaskTextBox"; + this.newTaskTextBox.Size = new System.Drawing.Size(540, 20); + this.newTaskTextBox.TabIndex = 1; + this.newTaskTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.newTaskTextBox_KeyPress); + // + // addTaskButton + // + this.addTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.addTaskButton.Location = new System.Drawing.Point(558, 376); + this.addTaskButton.Name = "addTaskButton"; + this.addTaskButton.Size = new System.Drawing.Size(100, 23); + this.addTaskButton.TabIndex = 2; + this.addTaskButton.Text = "Add Task"; + this.addTaskButton.UseVisualStyleBackColor = true; + this.addTaskButton.Click += new System.EventHandler(this.addTaskButton_Click); + // + // editTaskButton + // + this.editTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.editTaskButton.Location = new System.Drawing.Point(664, 376); + this.editTaskButton.Name = "editTaskButton"; + this.editTaskButton.Size = new System.Drawing.Size(100, 23); + this.editTaskButton.TabIndex = 3; + this.editTaskButton.Text = "Edit Task"; + this.editTaskButton.UseVisualStyleBackColor = true; + this.editTaskButton.Click += new System.EventHandler(this.editTaskButton_Click); + // + // deleteTaskButton + // + this.deleteTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.deleteTaskButton.Location = new System.Drawing.Point(664, 405); + this.deleteTaskButton.Name = "deleteTaskButton"; + this.deleteTaskButton.Size = new System.Drawing.Size(100, 23); + this.deleteTaskButton.TabIndex = 4; + this.deleteTaskButton.Text = "Delete Task"; + this.deleteTaskButton.UseVisualStyleBackColor = true; + this.deleteTaskButton.Click += new System.EventHandler(this.deleteTaskButton_Click); + // + // statusLabel + // + this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.statusLabel.AutoSize = true; + this.statusLabel.Location = new System.Drawing.Point(12, 410); + this.statusLabel.Name = "statusLabel"; + this.statusLabel.Size = new System.Drawing.Size(79, 13); + this.statusLabel.TabIndex = 5; + this.statusLabel.Text = "Status: Ready"; + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(784, 441); + this.Controls.Add(this.statusLabel); + this.Controls.Add(this.deleteTaskButton); + this.Controls.Add(this.editTaskButton); + this.Controls.Add(this.addTaskButton); + this.Controls.Add(this.newTaskTextBox); + this.Controls.Add(this.tasksListView); + this.Name = "MainForm"; + this.Text = "Tasks"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); + this.Load += new System.EventHandler(this.MainForm_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ListView tasksListView; + private System.Windows.Forms.ColumnHeader doneColumn; + private System.Windows.Forms.ColumnHeader titleColumn; + private System.Windows.Forms.ColumnHeader idColumn; + private System.Windows.Forms.TextBox newTaskTextBox; + private System.Windows.Forms.Button addTaskButton; + private System.Windows.Forms.Button editTaskButton; + private System.Windows.Forms.Button deleteTaskButton; + private System.Windows.Forms.Label statusLabel; + } +} + diff --git a/dotnet-winforms-net48/MainForm.cs b/dotnet-winforms-net48/MainForm.cs index a69a70339..493cd6bf7 100644 --- a/dotnet-winforms-net48/MainForm.cs +++ b/dotnet-winforms-net48/MainForm.cs @@ -1,253 +1,253 @@ -ο»Ώusing System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace Taskapp.WinForms.Net48 -{ - public partial class MainForm : Form - { - private bool _isUpdatingListView = false; - - public MainForm() - { - InitializeComponent(); - } - - private void MainForm_Load(object sender, EventArgs e) - { - // Enable double buffering to reduce flicker - typeof(ListView).InvokeMember("DoubleBuffered", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, - null, tasksListView, new object[] { true }); - - // Register observer to watch for task changes - TasksPeerService.Instance.ObserveTasksCollection(OnTasksChanged); - - // Update status - UpdateSyncStatus(); - } - - private async Task OnTasksChanged(IList tasks) - { - // Invoke on UI thread - if (InvokeRequired) - { - Invoke(new Action(() => UpdateTasksList(tasks))); - } - else - { - UpdateTasksList(tasks); - } - - await Task.CompletedTask; - } - - private void UpdateTasksList(IList tasks) - { - _isUpdatingListView = true; - tasksListView.BeginUpdate(); - - try - { - // Filter out null tasks and tasks with null/empty Id - var validTasks = tasks - .Where(t => t != null && !string.IsNullOrEmpty(t.Id)) - .ToList(); - - // Create dictionary of current items by ID for quick lookup - var currentItems = new Dictionary(); - foreach (ListViewItem item in tasksListView.Items) - { - var id = item.SubItems[2].Text; // ID is in third column - currentItems[id] = item; - } - - // Create dictionary of incoming tasks by ID - var incomingTasks = validTasks.ToDictionary(t => t.Id); - - // Update existing items and add new ones - foreach (var task in validTasks) - { - if (currentItems.ContainsKey(task.Id)) - { - // Update existing item - var item = currentItems[task.Id]; - item.SubItems[1].Text = task.Title; - item.Checked = task.Done; - } - else - { - // Add new item - var item = new ListViewItem(); - item.Checked = task.Done; - item.SubItems.Add(task.Title); - item.SubItems.Add(task.Id); - tasksListView.Items.Add(item); - } - } - - // Remove items that are no longer in the incoming list - var itemsToRemove = new List(); - foreach (ListViewItem item in tasksListView.Items) - { - var id = item.SubItems[2].Text; - if (!incomingTasks.ContainsKey(id)) - { - itemsToRemove.Add(item); - } - } - - foreach (var item in itemsToRemove) - { - tasksListView.Items.Remove(item); - } - } - finally - { - tasksListView.EndUpdate(); - _isUpdatingListView = false; - } - } - - private async void addTaskButton_Click(object sender, EventArgs e) - { - var title = newTaskTextBox.Text.Trim(); - if (string.IsNullOrWhiteSpace(title)) - { - MessageBox.Show("Please enter a task title.", "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - - try - { - await TasksPeerService.Instance.AddTaskAsync(title); - newTaskTextBox.Clear(); - UpdateStatus("Task added successfully"); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to add task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - - private async void editTaskButton_Click(object sender, EventArgs e) - { - if (tasksListView.SelectedItems.Count == 0) - { - MessageBox.Show("Please select a task to edit.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - var selectedItem = tasksListView.SelectedItems[0]; - var taskId = selectedItem.SubItems[2].Text; - var currentTitle = selectedItem.SubItems[1].Text; - - var newTitle = InputDialog.Show("Enter new title:", "Edit Task", currentTitle); - if (string.IsNullOrWhiteSpace(newTitle) || newTitle == currentTitle) - { - return; - } - - try - { - await TasksPeerService.Instance.UpdateTaskTitleAsync(taskId, newTitle); - UpdateStatus("Task updated successfully"); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to update task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - - private async void deleteTaskButton_Click(object sender, EventArgs e) - { - if (tasksListView.SelectedItems.Count == 0) - { - MessageBox.Show("Please select a task to delete.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - var selectedItem = tasksListView.SelectedItems[0]; - var taskTitle = selectedItem.SubItems[1].Text; - var taskId = selectedItem.SubItems[2].Text; - - var result = MessageBox.Show( - $"Are you sure you want to delete the task '{taskTitle}'?", - "Confirm Delete", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question - ); - - if (result == DialogResult.Yes) - { - try - { - await TasksPeerService.Instance.DeleteTaskAsync(taskId); - UpdateStatus("Task deleted successfully"); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to delete task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - } - - private async void tasksListView_ItemChecked(object sender, ItemCheckedEventArgs e) - { - // Prevent recursive updates (including the revert flip below) - if (_isUpdatingListView) - return; - - var item = e.Item; - var taskId = item.SubItems[2].Text; - var newCheckedState = item.Checked; - - try - { - await TasksPeerService.Instance.UpdateTaskDoneAsync(taskId, newCheckedState); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to update task status: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - // Revert the check state; guard prevents this flip from re-entering - _isUpdatingListView = true; - item.Checked = !newCheckedState; - _isUpdatingListView = false; - } - } - - private void newTaskTextBox_KeyPress(object sender, KeyPressEventArgs e) - { - // Allow adding task with Enter key - if (e.KeyChar == (char)Keys.Enter) - { - e.Handled = true; - addTaskButton_Click(sender, EventArgs.Empty); - } - } - - private void UpdateStatus(string message) - { - statusLabel.Text = $"Status: {message}"; - } - - private void UpdateSyncStatus() - { - if (TasksPeerService.Instance.IsSyncActive) - { - statusLabel.Text = "Status: Syncing"; - } - else - { - statusLabel.Text = "Status: Ready"; - } - } - - private void MainForm_FormClosing(object sender, FormClosingEventArgs e) - { - // Cleanup handled in Program.cs - } - } -} +ο»Ώusing System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Taskapp.WinForms.Net48 +{ + public partial class MainForm : Form + { + private bool _isUpdatingListView = false; + + public MainForm() + { + InitializeComponent(); + } + + private void MainForm_Load(object sender, EventArgs e) + { + // Enable double buffering to reduce flicker + typeof(ListView).InvokeMember("DoubleBuffered", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, + null, tasksListView, new object[] { true }); + + // Register observer to watch for task changes + TasksPeerService.Instance.ObserveTasksCollection(OnTasksChanged); + + // Update status + UpdateSyncStatus(); + } + + private async Task OnTasksChanged(IList tasks) + { + // Invoke on UI thread + if (InvokeRequired) + { + Invoke(new Action(() => UpdateTasksList(tasks))); + } + else + { + UpdateTasksList(tasks); + } + + await Task.CompletedTask; + } + + private void UpdateTasksList(IList tasks) + { + _isUpdatingListView = true; + tasksListView.BeginUpdate(); + + try + { + // Filter out null tasks and tasks with null/empty Id + var validTasks = tasks + .Where(t => t != null && !string.IsNullOrEmpty(t.Id)) + .ToList(); + + // Create dictionary of current items by ID for quick lookup + var currentItems = new Dictionary(); + foreach (ListViewItem item in tasksListView.Items) + { + var id = item.SubItems[2].Text; // ID is in third column + currentItems[id] = item; + } + + // Create dictionary of incoming tasks by ID + var incomingTasks = validTasks.ToDictionary(t => t.Id); + + // Update existing items and add new ones + foreach (var task in validTasks) + { + if (currentItems.ContainsKey(task.Id)) + { + // Update existing item + var item = currentItems[task.Id]; + item.SubItems[1].Text = task.Title; + item.Checked = task.Done; + } + else + { + // Add new item + var item = new ListViewItem(); + item.Checked = task.Done; + item.SubItems.Add(task.Title); + item.SubItems.Add(task.Id); + tasksListView.Items.Add(item); + } + } + + // Remove items that are no longer in the incoming list + var itemsToRemove = new List(); + foreach (ListViewItem item in tasksListView.Items) + { + var id = item.SubItems[2].Text; + if (!incomingTasks.ContainsKey(id)) + { + itemsToRemove.Add(item); + } + } + + foreach (var item in itemsToRemove) + { + tasksListView.Items.Remove(item); + } + } + finally + { + tasksListView.EndUpdate(); + _isUpdatingListView = false; + } + } + + private async void addTaskButton_Click(object sender, EventArgs e) + { + var title = newTaskTextBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(title)) + { + MessageBox.Show("Please enter a task title.", "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + try + { + await TasksPeerService.Instance.AddTaskAsync(title); + newTaskTextBox.Clear(); + UpdateStatus("Task added successfully"); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to add task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private async void editTaskButton_Click(object sender, EventArgs e) + { + if (tasksListView.SelectedItems.Count == 0) + { + MessageBox.Show("Please select a task to edit.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var selectedItem = tasksListView.SelectedItems[0]; + var taskId = selectedItem.SubItems[2].Text; + var currentTitle = selectedItem.SubItems[1].Text; + + var newTitle = InputDialog.Show("Enter new title:", "Edit Task", currentTitle); + if (string.IsNullOrWhiteSpace(newTitle) || newTitle == currentTitle) + { + return; + } + + try + { + await TasksPeerService.Instance.UpdateTaskTitleAsync(taskId, newTitle); + UpdateStatus("Task updated successfully"); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to update task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private async void deleteTaskButton_Click(object sender, EventArgs e) + { + if (tasksListView.SelectedItems.Count == 0) + { + MessageBox.Show("Please select a task to delete.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var selectedItem = tasksListView.SelectedItems[0]; + var taskTitle = selectedItem.SubItems[1].Text; + var taskId = selectedItem.SubItems[2].Text; + + var result = MessageBox.Show( + $"Are you sure you want to delete the task '{taskTitle}'?", + "Confirm Delete", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question + ); + + if (result == DialogResult.Yes) + { + try + { + await TasksPeerService.Instance.DeleteTaskAsync(taskId); + UpdateStatus("Task deleted successfully"); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to delete task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private async void tasksListView_ItemChecked(object sender, ItemCheckedEventArgs e) + { + // Prevent recursive updates (including the revert flip below) + if (_isUpdatingListView) + return; + + var item = e.Item; + var taskId = item.SubItems[2].Text; + var newCheckedState = item.Checked; + + try + { + await TasksPeerService.Instance.UpdateTaskDoneAsync(taskId, newCheckedState); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to update task status: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + // Revert the check state; guard prevents this flip from re-entering + _isUpdatingListView = true; + item.Checked = !newCheckedState; + _isUpdatingListView = false; + } + } + + private void newTaskTextBox_KeyPress(object sender, KeyPressEventArgs e) + { + // Allow adding task with Enter key + if (e.KeyChar == (char)Keys.Enter) + { + e.Handled = true; + addTaskButton_Click(sender, EventArgs.Empty); + } + } + + private void UpdateStatus(string message) + { + statusLabel.Text = $"Status: {message}"; + } + + private void UpdateSyncStatus() + { + if (TasksPeerService.Instance.IsSyncActive) + { + statusLabel.Text = "Status: Syncing"; + } + else + { + statusLabel.Text = "Status: Ready"; + } + } + + private void MainForm_FormClosing(object sender, FormClosingEventArgs e) + { + // Cleanup handled in Program.cs + } + } +} diff --git a/dotnet-winforms-net48/MainForm.resx b/dotnet-winforms-net48/MainForm.resx index 29dcb1b3a..1af7de150 100644 --- a/dotnet-winforms-net48/MainForm.resx +++ b/dotnet-winforms-net48/MainForm.resx @@ -1,120 +1,120 @@ -ο»Ώ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - +ο»Ώ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/dotnet-winforms-net48/Program.cs b/dotnet-winforms-net48/Program.cs index e5b69f174..77a67b55c 100644 --- a/dotnet-winforms-net48/Program.cs +++ b/dotnet-winforms-net48/Program.cs @@ -1,68 +1,68 @@ -ο»Ώusing System; -using System.Windows.Forms; - -namespace Taskapp.WinForms.Net48 -{ - internal static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - - try - { - // Load configuration from .env file - AppConfiguration.Load(); - - // Initialize TasksPeerService asynchronously - var initTask = TasksPeerService.Instance.InitializeAsync( - AppConfiguration.AppId, - AppConfiguration.PlaygroundToken, - AppConfiguration.AuthUrl - ); - - // Show loading form while initializing - using (var loadingForm = new LoadingForm(initTask)) - { - if (loadingForm.ShowDialog() == DialogResult.OK) - { - // Initialization successful, show main form - Application.Run(new MainForm()); - } - else - { - // Initialization failed or was cancelled - MessageBox.Show( - "Failed to initialize Ditto. Please check your configuration and try again.", - "Initialization Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - } - } - } - catch (Exception ex) - { - MessageBox.Show( - $"Application startup failed: {ex.Message}", - "Startup Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - } - finally - { - // Cleanup on exit - if (TasksPeerService.Instance.IsInitialized) - { - TasksPeerService.Instance.Dispose(); - } - } - } - } -} +ο»Ώusing System; +using System.Windows.Forms; + +namespace Taskapp.WinForms.Net48 +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + try + { + // Load configuration from .env file + AppConfiguration.Load(); + + // Initialize TasksPeerService asynchronously + var initTask = TasksPeerService.Instance.InitializeAsync( + AppConfiguration.DatabaseId, + AppConfiguration.DevelopmentToken, + AppConfiguration.ServerUrl + ); + + // Show loading form while initializing + using (var loadingForm = new LoadingForm(initTask)) + { + if (loadingForm.ShowDialog() == DialogResult.OK) + { + // Initialization successful, show main form + Application.Run(new MainForm()); + } + else + { + // Initialization failed or was cancelled + MessageBox.Show( + "Failed to initialize Ditto. Please check your configuration and try again.", + "Initialization Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + } + } + catch (Exception ex) + { + MessageBox.Show( + $"Application startup failed: {ex.Message}", + "Startup Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + finally + { + // Cleanup on exit + if (TasksPeerService.Instance.IsInitialized) + { + TasksPeerService.Instance.Dispose(); + } + } + } + } +} diff --git a/dotnet-winforms-net48/Properties/AssemblyInfo.cs b/dotnet-winforms-net48/Properties/AssemblyInfo.cs index 94f3c000b..e4fb49ca4 100644 --- a/dotnet-winforms-net48/Properties/AssemblyInfo.cs +++ b/dotnet-winforms-net48/Properties/AssemblyInfo.cs @@ -1,33 +1,33 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Taskapp.WinForms.Net48")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Ditto")] -[assembly: AssemblyProduct("Taskapp.WinForms.Net48")] -[assembly: AssemblyCopyright("Copyright Β© Ditto Live 2026")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("9f61044a-836b-42c3-974e-1cca09ee86a2")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +ο»Ώusing System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Taskapp.WinForms.Net48")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Ditto")] +[assembly: AssemblyProduct("Taskapp.WinForms.Net48")] +[assembly: AssemblyCopyright("Copyright Β© Ditto Live 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9f61044a-836b-42c3-974e-1cca09ee86a2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/dotnet-winforms-net48/Properties/Resources.Designer.cs b/dotnet-winforms-net48/Properties/Resources.Designer.cs index c6bdade56..dc069845d 100644 --- a/dotnet-winforms-net48/Properties/Resources.Designer.cs +++ b/dotnet-winforms-net48/Properties/Resources.Designer.cs @@ -1,71 +1,71 @@ -ο»Ώ//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Taskapp.WinForms.Net48.Properties -{ - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Taskapp.WinForms.Net48.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - } -} +ο»Ώ//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Taskapp.WinForms.Net48.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Taskapp.WinForms.Net48.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/dotnet-winforms-net48/Properties/Resources.resx b/dotnet-winforms-net48/Properties/Resources.resx index ffecec851..af7dbebba 100644 --- a/dotnet-winforms-net48/Properties/Resources.resx +++ b/dotnet-winforms-net48/Properties/Resources.resx @@ -1,117 +1,117 @@ -ο»Ώ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - +ο»Ώ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/dotnet-winforms-net48/Properties/Settings.Designer.cs b/dotnet-winforms-net48/Properties/Settings.Designer.cs index 28a61c00a..761f2f69a 100644 --- a/dotnet-winforms-net48/Properties/Settings.Designer.cs +++ b/dotnet-winforms-net48/Properties/Settings.Designer.cs @@ -1,30 +1,30 @@ -ο»Ώ//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Taskapp.WinForms.Net48.Properties -{ - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} +ο»Ώ//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Taskapp.WinForms.Net48.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/dotnet-winforms-net48/Properties/Settings.settings b/dotnet-winforms-net48/Properties/Settings.settings index abf36c5d3..39645652a 100644 --- a/dotnet-winforms-net48/Properties/Settings.settings +++ b/dotnet-winforms-net48/Properties/Settings.settings @@ -1,7 +1,7 @@ -ο»Ώ - - - - - - +ο»Ώ + + + + + + diff --git a/dotnet-winforms-net48/README.md b/dotnet-winforms-net48/README.md index 999744a3f..b0d7b39f8 100644 --- a/dotnet-winforms-net48/README.md +++ b/dotnet-winforms-net48/README.md @@ -7,14 +7,14 @@ - Does not support ARM64 (however, it can run on Windows on ARM using x64 emulation if supported by the OS/hardware). ARM64 is not officially supported by Ditto for use with the .NET 4.8 framework. 1. Install the .NET 4.8 SDK from -2. Create an application at . Make note of the app ID and online playground token -3. Copy `.env.sample` from the repo root into the `dotnet-winforms-net48/` folder, rename the copy to `.env`, and fill in your `DITTO_APP_ID`, `DITTO_PLAYGROUND_TOKEN`, and `DITTO_AUTH_URL` (from ). The `.env` file must live next to `Taskapp.WinForms.Net48.csproj` β€” this app reads its config from the project folder, not the repo root. (`DITTO_WEBSOCKET_URL` is in the sample but unused by this app; leave it blank.) +2. Create an application at . Make note of the Database ID and development token +3. Copy `.env.sample` from the repo root into the `dotnet-winforms-net48/` folder, rename the copy to `.env`, and fill in your `DITTO_DATABASE_ID`, `DITTO_DEVELOPMENT_TOKEN`, and `DITTO_SERVER_URL` (from ). The `.env` file must live next to `Taskapp.WinForms.Net48.csproj` β€” this app reads its config from the project folder, not the repo root. ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.1/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ## .NET Windows Forms Application diff --git a/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj b/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj index 7f60ecbc5..9370017c7 100644 --- a/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj +++ b/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj @@ -1,161 +1,160 @@ - - - - - Debug - AnyCPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2} - WinExe - Taskapp.WinForms.Net48 - Taskapp.WinForms.Net48 - v4.8 - 512 - true - true - - - x64 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - x64 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - packages\PeterO.Cbor.4.5.2\lib\net40\CBOR.dll - - - packages\Ditto.5.0.1\lib\netstandard2.0\Ditto.dll - - - packages\Ditto.Native.Linux.5.0.1\lib\netstandard2.0\Ditto.Native.Linux.dll - - - packages\Ditto.Native.MacOS.5.0.1\lib\netstandard2.0\Ditto.Native.MacOS.dll - - - packages\Ditto.Native.Win.5.0.1\lib\netstandard2.0\Ditto.Native.Win.dll - - - packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll - - - packages\Microsoft.Bcl.HashCode.1.1.1\lib\net461\Microsoft.Bcl.HashCode.dll - - - packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll - - - packages\PeterO.Numbers.1.8.2\lib\net40\Numbers.dll - - - - packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - - packages\System.Memory.4.5.5\lib\net461\System.Memory.dll - - - - packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll - - - packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll - - - packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - - - - - - - - - - - - packages\PeterO.URIUtility.1.0.0\lib\net40\URIUtility.dll - - - - - - - Form - - - LoadingForm.cs - - - Form - - - MainForm.cs - - - - - - - - MainForm.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - PreserveNewest - - - - - dittoffi.dll - PreserveNewest - - - + + + + + Debug + AnyCPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2} + WinExe + Taskapp.WinForms.Net48 + Taskapp.WinForms.Net48 + v4.8 + 512 + true + true + + + x64 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + x64 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + packages\PeterO.Cbor.4.5.2\lib\net40\CBOR.dll + + + + packages\Ditto.5.1.0\lib\netstandard2.0\Ditto.dll + False + + + packages\Ditto.Native.Linux.5.1.0\lib\netstandard2.0\Ditto.Native.Linux.dll + + + packages\Ditto.Native.MacOS.5.1.0\lib\netstandard2.0\Ditto.Native.MacOS.dll + + + packages\Ditto.Native.Win.5.1.0\lib\netstandard2.0\Ditto.Native.Win.dll + + + packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + + packages\Microsoft.Bcl.HashCode.1.1.1\lib\net461\Microsoft.Bcl.HashCode.dll + + + packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll + + + packages\PeterO.Numbers.1.8.2\lib\net40\Numbers.dll + + + + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + + packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll + + + packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll + + + packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + + + + + + + + + + + + packages\PeterO.URIUtility.1.0.0\lib\net40\URIUtility.dll + + + + + + + Form + + + LoadingForm.cs + + + Form + + + MainForm.cs + + + + + + + + MainForm.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln b/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln index d9d5c4ad9..782f61171 100644 --- a/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln +++ b/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln @@ -1,25 +1,25 @@ -ο»Ώ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28729.10 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskapp.WinForms.Net48", "Taskapp.WinForms.Net48.csproj", "{9F61044A-836B-42C3-974E-1CCA09EE86A2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FEC06644-F3C3-4034-843B-D871F0B794C8} - EndGlobalSection -EndGlobal +ο»Ώ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28729.10 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskapp.WinForms.Net48", "Taskapp.WinForms.Net48.csproj", "{9F61044A-836B-42C3-974E-1CCA09EE86A2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FEC06644-F3C3-4034-843B-D871F0B794C8} + EndGlobalSection +EndGlobal diff --git a/dotnet-winforms-net48/TasksPeer.cs b/dotnet-winforms-net48/TasksPeer.cs index 4d57b06f9..434c00424 100644 --- a/dotnet-winforms-net48/TasksPeer.cs +++ b/dotnet-winforms-net48/TasksPeer.cs @@ -1,290 +1,246 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -using DittoSDK; -using DittoSDK.Auth; -using DittoSDK.Store; -using Newtonsoft.Json; - -/// -/// Encapsulates use of the Ditto SDK and the 'tasks' collection. -/// - -namespace Taskapp.WinForms.Net48 -{ - public class TasksPeer : IDisposable - { - private const string Query = "SELECT * FROM tasks WHERE NOT deleted"; - - public string AppId { get; private set; } - public string PlaygroundToken { get; private set; } - public string AuthUrl { get; private set; } - - public bool IsSyncActive => _ditto.Sync.IsActive; - - private readonly Ditto _ditto; - - /// - /// Creates a new synchronizing TasksPeer instance. - /// - public static async Task Create( - string appId, - string playgroundToken, - string authUrl) - { - var peer = new TasksPeer(appId, playgroundToken, authUrl); - peer.Authenticate(); - peer.RegisterSubscription(); - await peer.InsertInitialTasks(); - peer.StartSync(); - - return peer; - } - - private void Authenticate() - { - _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => - { - // Authenticate when token is expiring - try - { - await ditto.Auth.LoginAsync( - // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider - DittoAuthenticationProvider.Development - ); - Console.WriteLine("Authentication successful"); - } - catch (Exception error) - { - Console.WriteLine($"Authentication failed: {error}"); - } - }; - } - - /// - /// Registers a subscription for the tasks collection to enable data synchronization - /// with other peers and the Ditto cloud. This subscription determines what data - /// will be synced to this peer during the synchronization process. - /// - /// - /// The subscription is created using the same query that filters out deleted tasks - /// (SELECT * FROM tasks WHERE NOT deleted), ensuring that only active, - /// non-deleted tasks are synchronized across the network. - /// - /// This method should be called during peer initialization to establish the - /// subscription before starting the sync process. The subscription remains active - /// until explicitly cancelled or when the peer is disposed. - /// - /// - private void RegisterSubscription() - { - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - _ditto.Sync.RegisterSubscription(Query); - } - - /// - /// Constructor - /// - /// Ditto application ID - /// Ditto online playground token - /// Ditto Auth URL - private TasksPeer(string appId, string playgroundToken, string authUrl) - { - AppId = appId; - PlaygroundToken = playgroundToken; - AuthUrl = authUrl; - - var config = new DittoConfig( - AppId, - new DittoConfigConnect.Server(new Uri(authUrl)) - ); - - _ditto = Ditto.Open(config); - } - - public void Dispose() - { - _ditto.Dispose(); - GC.SuppressFinalize(this); - } - - /// - /// Inserts the initial tasks into the 'tasks' collection. - /// - private async Task InsertInitialTasks() - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - const string insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - - /// - /// Adds a new task to the 'tasks' collection. - /// - public async Task AddTask(string title) - { - if (string.IsNullOrWhiteSpace(title)) - { - throw new ArgumentException("title cannot be empty"); - } - - var doc = new Dictionary - { - {"title", title}, - {"done", false}, - {"deleted", false } - }; - const string insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "doc", doc } - }); - } - - /// - /// Update the title of an existing task. - /// - public async Task UpdateTaskTitle(string taskId, string newTitle) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - if (string.IsNullOrWhiteSpace(newTitle)) - { - throw new ArgumentException("title cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - {"title", newTitle}, - {"id", taskId} - }); - } - - /// - /// Mark a task as deleted. - /// - public async Task DeleteTask(string taskId) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - { "id", taskId } - }); - } - - /// - /// Mark a task as complete or not complete. - /// - public async Task UpdateTaskDone(string taskId, bool newDoneState) - { - const string updateQuery = "UPDATE tasks SET done = :newDoneState WHERE _id = :id AND done != :newDoneState"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary - { - { "newDoneState", newDoneState }, - { "id", taskId } - }); - } - - /// - /// Specify a handler to be called asynchronously when the task collection changes. - /// - public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) - { - // Register an observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - return _ditto.Store.RegisterObserver(Query, async (queryResult) => - { - try - { - // Deserialize the JSON documents into ToDoTask objects - var tasks = queryResult.Items.Select(d => - { - using (var reader = new StringReader(d.JsonString())) - using (var jsonReader = new JsonTextReader(reader)) - { - return JsonSerializer.Create().Deserialize(jsonReader); - } - }).OrderBy(t => t?.Id).ToList(); - - await handler(tasks); - } - catch (Exception e) - { - Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); - } - }); - } - - /// - /// Start synchronizing the 'tasks' collection. - /// - public void StartSync() - { - _ditto.Sync.Start(); - } - - /// - /// Stop synchronizing the 'tasks' collection. - /// - public void StopSync() - { - foreach (var subscription in _ditto.Sync.Subscriptions) - { - subscription.Cancel(); - } - _ditto.Sync.Stop(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using DittoSDK; +using DittoSDK.Auth; +using DittoSDK.Store; +using Newtonsoft.Json; + +/// +/// Encapsulates use of the Ditto SDK and the 'tasks' collection. +/// + +namespace Taskapp.WinForms.Net48 +{ + public class TasksPeer : IDisposable + { + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; + + public string DatabaseId { get; private set; } + public string DevelopmentToken { get; private set; } + public string ServerUrl { get; private set; } + + public bool IsSyncActive => _ditto.Sync.IsActive; + + private readonly Ditto _ditto; + + /// + /// Creates a new synchronizing TasksPeer instance. + /// + public static Task Create( + string databaseId, + string developmentToken, + string serverUrl) + { + var peer = new TasksPeer(databaseId, developmentToken, serverUrl); + peer.Authenticate(); + peer.RegisterSubscription(); + peer.StartSync(); + + return Task.FromResult(peer); + } + + private void Authenticate() + { + _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => + { + // Authenticate when token is expiring + try + { + await ditto.Auth.LoginAsync( + // Your development token, replace with your actual token + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider + DittoAuthenticationProvider.Development + ); + Console.WriteLine("Authentication successful"); + } + catch (Exception error) + { + Console.WriteLine($"Authentication failed: {error}"); + } + }; + } + + /// + /// Registers a subscription for the tasks collection to enable data synchronization + /// with other peers and the Ditto cloud. This subscription determines what data + /// will be synced to this peer during the synchronization process. + /// + /// + /// The subscription syncs all tasks (SELECT * FROM tasks) and controls + /// what is written to this peer's local database; the observer query hides + /// soft-deleted tasks from the displayed list. + /// + /// This method should be called during peer initialization to establish the + /// subscription before starting the sync process. The subscription remains active + /// until explicitly cancelled or when the peer is disposed. + /// + /// + private void RegisterSubscription() + { + // Register a subscription, which determines what data syncs to this peer + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + _ditto.Sync.RegisterSubscription(SubscriptionQuery); + } + + /// + /// Constructor + /// + /// Ditto database ID + /// Ditto development token + /// Ditto Server URL + private TasksPeer(string databaseId, string developmentToken, string serverUrl) + { + DatabaseId = databaseId; + DevelopmentToken = developmentToken; + ServerUrl = serverUrl; + + var config = new DittoConfig( + DatabaseId, + new DittoConfigConnect.Server(new Uri(serverUrl)) + ); + + _ditto = Ditto.Open(config); + } + + public void Dispose() + { + _ditto.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Adds a new task to the 'tasks' collection. + /// + public async Task AddTask(string title) + { + if (string.IsNullOrWhiteSpace(title)) + { + throw new ArgumentException("title cannot be empty"); + } + + var doc = new Dictionary + { + {"title", title}, + {"done", false}, + {"deleted", false } + }; + const string insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; + await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() + { + { "task", doc } + }); + } + + /// + /// Update the title of an existing task. + /// + public async Task UpdateTaskTitle(string taskId, string newTitle) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + if (string.IsNullOrWhiteSpace(newTitle)) + { + throw new ArgumentException("title cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; + await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + {"title", newTitle}, + {"id", taskId} + }); + } + + /// + /// Mark a task as deleted. + /// + public async Task DeleteTask(string taskId) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; + await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + { "id", taskId } + }); + } + + /// + /// Mark a task as complete or not complete. + /// + public async Task UpdateTaskDone(string taskId, bool newDoneState) + { + const string updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; + await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary + { + { "done", newDoneState }, + { "id", taskId } + }); + } + + /// + /// Specify a handler to be called asynchronously when the task collection changes. + /// + public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) + { + // Register an observer, which runs against the local database on this peer + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + return _ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => + { + try + { + // Deserialize the JSON documents into ToDoTask objects + var tasks = queryResult.Items.Select(d => + { + using (var reader = new StringReader(d.JsonString())) + using (var jsonReader = new JsonTextReader(reader)) + { + return JsonSerializer.Create().Deserialize(jsonReader); + } + }).OrderBy(t => t?.Id).ToList(); + + await handler(tasks); + } + catch (Exception e) + { + Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); + } + }); + } + + /// + /// Start synchronizing the 'tasks' collection. + /// + public void StartSync() + { + _ditto.Sync.Start(); + } + + /// + /// Stop synchronizing the 'tasks' collection. + /// + public void StopSync() + { + foreach (var subscription in _ditto.Sync.Subscriptions) + { + subscription.Cancel(); + } + _ditto.Sync.Stop(); + } + } +} diff --git a/dotnet-winforms-net48/TasksPeerService.cs b/dotnet-winforms-net48/TasksPeerService.cs index 628ca94dd..92666b868 100644 --- a/dotnet-winforms-net48/TasksPeerService.cs +++ b/dotnet-winforms-net48/TasksPeerService.cs @@ -27,12 +27,12 @@ private TasksPeerService() /// /// Initializes the TasksPeer with configuration values /// - public async Task InitializeAsync(string appId, string playgroundToken, string authUrl) + public async Task InitializeAsync(string databaseId, string developmentToken, string serverUrl) { if (_isInitialized) throw new InvalidOperationException("TasksPeerService is already initialized"); - _tasksPeer = await TasksPeer.Create(appId, playgroundToken, authUrl); + _tasksPeer = await TasksPeer.Create(databaseId, developmentToken, serverUrl); _isInitialized = true; } diff --git a/dotnet-winforms-net48/ToDoTask.cs b/dotnet-winforms-net48/ToDoTask.cs index 109c553a4..b532aa63c 100644 --- a/dotnet-winforms-net48/ToDoTask.cs +++ b/dotnet-winforms-net48/ToDoTask.cs @@ -1,29 +1,29 @@ -using Newtonsoft.Json; -using System; - -namespace Taskapp.WinForms.Net48 -{ - /// - /// Representation of a document in the Ditto 'tasks' collection. - /// - /// - /// This class is named ToDoTask rather than Task to avoid - /// conflicts with the System.Threading.Tasks.Task class. - /// - public class ToDoTask - { - [JsonProperty("_id")] - public string Id { get; set; } - - [JsonProperty("title")] - public string Title { get; set; } - - [JsonProperty("done")] - public bool Done { get; set; } - - [JsonProperty("deleted")] - public bool Deleted { get; set; } - - override public string ToString() => Title; - } -} +using Newtonsoft.Json; +using System; + +namespace Taskapp.WinForms.Net48 +{ + /// + /// Representation of a document in the Ditto 'tasks' collection. + /// + /// + /// This class is named ToDoTask rather than Task to avoid + /// conflicts with the System.Threading.Tasks.Task class. + /// + public class ToDoTask + { + [JsonProperty("_id")] + public string Id { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("done")] + public bool Done { get; set; } + + [JsonProperty("deleted")] + public bool Deleted { get; set; } + + override public string ToString() => Title; + } +} diff --git a/dotnet-winforms-net48/packages.config b/dotnet-winforms-net48/packages.config index df16735fe..29ce61775 100644 --- a/dotnet-winforms-net48/packages.config +++ b/dotnet-winforms-net48/packages.config @@ -1,21 +1,21 @@ -ο»Ώ - - - - - - - - - - - - - - - - - - - +ο»Ώ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet-winforms/IntegrationTest/Program.cs b/dotnet-winforms/IntegrationTest/Program.cs index 5d7bfded4..a7e8b3e52 100644 --- a/dotnet-winforms/IntegrationTest/Program.cs +++ b/dotnet-winforms/IntegrationTest/Program.cs @@ -48,24 +48,23 @@ static async Task Main(string[] args) } // Get required environment variables - if (!envVars.TryGetValue("DITTO_APP_ID", out var appId) || string.IsNullOrEmpty(appId)) + if (!envVars.TryGetValue("DITTO_DATABASE_ID", out var databaseId) || string.IsNullOrEmpty(databaseId)) { - Console.WriteLine("❌ Missing DITTO_APP_ID in .env file"); + Console.WriteLine("❌ Missing DITTO_DATABASE_ID in .env file"); return 1; } - if (!envVars.TryGetValue("DITTO_PLAYGROUND_TOKEN", out var playgroundToken) || string.IsNullOrEmpty(playgroundToken)) + if (!envVars.TryGetValue("DITTO_DEVELOPMENT_TOKEN", out var developmentToken) || string.IsNullOrEmpty(developmentToken)) { - Console.WriteLine("❌ Missing DITTO_PLAYGROUND_TOKEN in .env file"); + Console.WriteLine("❌ Missing DITTO_DEVELOPMENT_TOKEN in .env file"); return 1; } - var authUrl = envVars.GetValueOrDefault("DITTO_AUTH_URL", "https://auth.cloud.ditto.live"); - var websocketUrl = envVars.GetValueOrDefault("DITTO_WEBSOCKET_URL", "wss://cloud.ditto.live"); + var serverUrl = envVars.GetValueOrDefault("DITTO_SERVER_URL", "https://auth.cloud.ditto.live"); - Console.WriteLine($"πŸ“‘ Connecting to Ditto (App ID: {appId})"); + Console.WriteLine($"πŸ“‘ Connecting to Ditto (Database ID: {databaseId})"); // Initialize TasksPeer and start sync - var tasksPeer = await TasksPeer.Create(appId, playgroundToken, authUrl, websocketUrl); + var tasksPeer = await TasksPeer.Create(databaseId, developmentToken, serverUrl); Console.WriteLine("βœ… Ditto initialized and sync started"); Console.WriteLine("⏳ Waiting for document to sync..."); @@ -147,4 +146,4 @@ static async Task Main(string[] args) return 1; } } -} \ No newline at end of file +} diff --git a/dotnet-winforms/README.md b/dotnet-winforms/README.md index 3c9ea476f..eb798d792 100644 --- a/dotnet-winforms/README.md +++ b/dotnet-winforms/README.md @@ -3,14 +3,14 @@ ## Prerequisites 1. Install the .NET 10 SDK from -2. Create an application at . Make note of the app ID and online playground token -3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your app ID and online playground token. +2. Create an application at . Make note of the Database ID and development token +3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your Database ID and development token. ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.0/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ## .NET Windows Forms Application diff --git a/dotnet-winforms/TasksApp/AboutForm.Designer.cs b/dotnet-winforms/TasksApp/AboutForm.Designer.cs index 3fb677aa7..cf66bfefd 100644 --- a/dotnet-winforms/TasksApp/AboutForm.Designer.cs +++ b/dotnet-winforms/TasksApp/AboutForm.Designer.cs @@ -40,14 +40,12 @@ private void InitializeComponent() llDittoDocs = new LinkLabel(); lblSDKVersionText = new Label(); lblSDKVersion = new Label(); - lblAppIDText = new Label(); - lblAppIdValue = new Label(); - lblOnlinePlaygroundTokenTxt = new Label(); - lblOnlinePlaygroundTokenValue = new Label(); - lblAuthUrlTxt = new Label(); - lblAuthURLValue = new Label(); - lblWebsocketUrlTxt = new Label(); - lblWebsocketURLValue = new Label(); + lblDatabaseIdText = new Label(); + lblDatabaseIdValue = new Label(); + lblDevelopmentTokenTxt = new Label(); + lblDevelopmentTokenValue = new Label(); + lblServerUrlTxt = new Label(); + lblServerURLValue = new Label(); btnClose = new Button(); ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); SuspendLayout(); @@ -94,86 +92,66 @@ private void InitializeComponent() lblSDKVersion.TabIndex = 3; lblSDKVersion.Text = "0.0.0.0"; // - // lblAppIDText - // - lblAppIDText.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAppIDText.AutoSize = true; - lblAppIDText.Location = new Point(12, 196); - lblAppIDText.Name = "lblAppIDText"; - lblAppIDText.Size = new Size(42, 15); - lblAppIDText.TabIndex = 4; - lblAppIDText.Text = "AppId:"; - // - // lblAppIdValue - // - lblAppIdValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAppIdValue.AutoSize = true; - lblAppIdValue.Location = new Point(189, 196); - lblAppIdValue.Name = "lblAppIdValue"; - lblAppIdValue.Size = new Size(46, 15); - lblAppIdValue.TabIndex = 5; - lblAppIdValue.Text = "Not Set"; - // - // lblOnlinePlaygroundTokenTxt - // - lblOnlinePlaygroundTokenTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblOnlinePlaygroundTokenTxt.AutoSize = true; - lblOnlinePlaygroundTokenTxt.Location = new Point(12, 225); - lblOnlinePlaygroundTokenTxt.Name = "lblOnlinePlaygroundTokenTxt"; - lblOnlinePlaygroundTokenTxt.Size = new Size(144, 15); - lblOnlinePlaygroundTokenTxt.TabIndex = 6; - lblOnlinePlaygroundTokenTxt.Text = "Online Playground Token:"; - // - // lblOnlinePlaygroundTokenValue - // - lblOnlinePlaygroundTokenValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblOnlinePlaygroundTokenValue.AutoSize = true; - lblOnlinePlaygroundTokenValue.Location = new Point(189, 225); - lblOnlinePlaygroundTokenValue.Name = "lblOnlinePlaygroundTokenValue"; - lblOnlinePlaygroundTokenValue.Size = new Size(46, 15); - lblOnlinePlaygroundTokenValue.TabIndex = 7; - lblOnlinePlaygroundTokenValue.Text = "Not Set"; - // - // lblAuthUrlTxt - // - lblAuthUrlTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAuthUrlTxt.AutoSize = true; - lblAuthUrlTxt.Location = new Point(12, 254); - lblAuthUrlTxt.Name = "lblAuthUrlTxt"; - lblAuthUrlTxt.Size = new Size(60, 15); - lblAuthUrlTxt.TabIndex = 8; - lblAuthUrlTxt.Text = "Auth URL:"; - // - // lblAuthURLValue - // - lblAuthURLValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAuthURLValue.AutoSize = true; - lblAuthURLValue.Location = new Point(189, 254); - lblAuthURLValue.Name = "lblAuthURLValue"; - lblAuthURLValue.Size = new Size(46, 15); - lblAuthURLValue.TabIndex = 9; - lblAuthURLValue.Text = "Not Set"; - // - // lblWebsocketUrlTxt - // - lblWebsocketUrlTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblWebsocketUrlTxt.AutoSize = true; - lblWebsocketUrlTxt.Location = new Point(12, 283); - lblWebsocketUrlTxt.Name = "lblWebsocketUrlTxt"; - lblWebsocketUrlTxt.Size = new Size(92, 15); - lblWebsocketUrlTxt.TabIndex = 10; - lblWebsocketUrlTxt.Text = "Websocket URL:"; - // - // lblWebsocketURLValue - // - lblWebsocketURLValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblWebsocketURLValue.AutoSize = true; - lblWebsocketURLValue.Location = new Point(189, 283); - lblWebsocketURLValue.Name = "lblWebsocketURLValue"; - lblWebsocketURLValue.Size = new Size(46, 15); - lblWebsocketURLValue.TabIndex = 11; - lblWebsocketURLValue.Text = "Not Set"; - // + // lblDatabaseIdText + // + lblDatabaseIdText.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDatabaseIdText.AutoSize = true; + lblDatabaseIdText.Location = new Point(12, 196); + lblDatabaseIdText.Name = "lblDatabaseIdText"; + lblDatabaseIdText.Size = new Size(42, 15); + lblDatabaseIdText.TabIndex = 4; + lblDatabaseIdText.Text = "Database ID:"; + // + // lblDatabaseIdValue + // + lblDatabaseIdValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDatabaseIdValue.AutoSize = true; + lblDatabaseIdValue.Location = new Point(189, 196); + lblDatabaseIdValue.Name = "lblDatabaseIdValue"; + lblDatabaseIdValue.Size = new Size(46, 15); + lblDatabaseIdValue.TabIndex = 5; + lblDatabaseIdValue.Text = "Not Set"; + // + // lblDevelopmentTokenTxt + // + lblDevelopmentTokenTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDevelopmentTokenTxt.AutoSize = true; + lblDevelopmentTokenTxt.Location = new Point(12, 225); + lblDevelopmentTokenTxt.Name = "lblDevelopmentTokenTxt"; + lblDevelopmentTokenTxt.Size = new Size(144, 15); + lblDevelopmentTokenTxt.TabIndex = 6; + lblDevelopmentTokenTxt.Text = "Development Token:"; + // + // lblDevelopmentTokenValue + // + lblDevelopmentTokenValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDevelopmentTokenValue.AutoSize = true; + lblDevelopmentTokenValue.Location = new Point(189, 225); + lblDevelopmentTokenValue.Name = "lblDevelopmentTokenValue"; + lblDevelopmentTokenValue.Size = new Size(46, 15); + lblDevelopmentTokenValue.TabIndex = 7; + lblDevelopmentTokenValue.Text = "Not Set"; + // + // lblServerUrlTxt + // + lblServerUrlTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblServerUrlTxt.AutoSize = true; + lblServerUrlTxt.Location = new Point(12, 254); + lblServerUrlTxt.Name = "lblServerUrlTxt"; + lblServerUrlTxt.Size = new Size(60, 15); + lblServerUrlTxt.TabIndex = 8; + lblServerUrlTxt.Text = "Server URL:"; + // + // lblServerURLValue + // + lblServerURLValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblServerURLValue.AutoSize = true; + lblServerURLValue.Location = new Point(189, 254); + lblServerURLValue.Name = "lblServerURLValue"; + lblServerURLValue.Size = new Size(46, 15); + lblServerURLValue.TabIndex = 9; + lblServerURLValue.Text = "Not Set"; + // // btnClose // btnClose.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; @@ -192,14 +170,12 @@ private void InitializeComponent() AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(584, 361); Controls.Add(btnClose); - Controls.Add(lblWebsocketURLValue); - Controls.Add(lblWebsocketUrlTxt); - Controls.Add(lblAuthURLValue); - Controls.Add(lblAuthUrlTxt); - Controls.Add(lblOnlinePlaygroundTokenValue); - Controls.Add(lblOnlinePlaygroundTokenTxt); - Controls.Add(lblAppIdValue); - Controls.Add(lblAppIDText); + Controls.Add(lblServerURLValue); + Controls.Add(lblServerUrlTxt); + Controls.Add(lblDevelopmentTokenValue); + Controls.Add(lblDevelopmentTokenTxt); + Controls.Add(lblDatabaseIdValue); + Controls.Add(lblDatabaseIdText); Controls.Add(lblSDKVersion); Controls.Add(lblSDKVersionText); Controls.Add(llDittoDocs); @@ -223,14 +199,12 @@ private void InitializeComponent() private LinkLabel llDittoDocs; private Label lblSDKVersionText; private Label lblSDKVersion; - private Label lblAppIDText; - private Label lblAppIdValue; - private Label lblOnlinePlaygroundTokenTxt; - private Label lblOnlinePlaygroundTokenValue; - private Label lblAuthUrlTxt; - private Label lblAuthURLValue; - private Label lblWebsocketUrlTxt; - private Label lblWebsocketURLValue; + private Label lblDatabaseIdText; + private Label lblDatabaseIdValue; + private Label lblDevelopmentTokenTxt; + private Label lblDevelopmentTokenValue; + private Label lblServerUrlTxt; + private Label lblServerURLValue; private Button btnClose; } } \ No newline at end of file diff --git a/dotnet-winforms/TasksApp/AboutForm.cs b/dotnet-winforms/TasksApp/AboutForm.cs index 8d2f72fa0..8e43ec085 100644 --- a/dotnet-winforms/TasksApp/AboutForm.cs +++ b/dotnet-winforms/TasksApp/AboutForm.cs @@ -27,10 +27,9 @@ public AboutForm(TasksPeer tasksPeer) /// private void SetFormValues() { - lblAppIdValue.Text = _tasksPeer.AppId; - lblAuthURLValue.Text = _tasksPeer.AuthUrl; - lblOnlinePlaygroundTokenValue.Text = _tasksPeer.PlaygroundToken; - lblWebsocketURLValue.Text = _tasksPeer.WebsocketUrl; + lblDatabaseIdValue.Text = _tasksPeer.DatabaseId; + lblServerURLValue.Text = _tasksPeer.ServerUrl; + lblDevelopmentTokenValue.Text = _tasksPeer.DevelopmentToken; // Get the version of the DittoSDK assembly var dittoAssembly = typeof(DittoSDK.Ditto).Assembly; diff --git a/dotnet-winforms/TasksApp/DittoTasksApp.csproj b/dotnet-winforms/TasksApp/DittoTasksApp.csproj index cc3a97312..03d803a9d 100644 --- a/dotnet-winforms/TasksApp/DittoTasksApp.csproj +++ b/dotnet-winforms/TasksApp/DittoTasksApp.csproj @@ -11,7 +11,7 @@ - + diff --git a/dotnet-winforms/TasksApp/MainForm.Designer.cs b/dotnet-winforms/TasksApp/MainForm.Designer.cs index bec6f5f0f..4f480ad54 100644 --- a/dotnet-winforms/TasksApp/MainForm.Designer.cs +++ b/dotnet-winforms/TasksApp/MainForm.Designer.cs @@ -33,7 +33,7 @@ private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); statusStrip1 = new StatusStrip(); - tsslAppId = new ToolStripStatusLabel(); + tsslDatabaseId = new ToolStripStatusLabel(); tsslAuthToken = new ToolStripStatusLabel(); menuStrip1 = new MenuStrip(); fileToolStripMenuItem = new ToolStripMenuItem(); @@ -68,19 +68,19 @@ private void InitializeComponent() // // statusStrip1 // - statusStrip1.Items.AddRange(new ToolStripItem[] { tsslAppId, tsslAuthToken }); + statusStrip1.Items.AddRange(new ToolStripItem[] { tsslDatabaseId, tsslAuthToken }); statusStrip1.Location = new Point(0, 707); statusStrip1.Name = "statusStrip1"; statusStrip1.Size = new Size(1008, 22); statusStrip1.TabIndex = 0; statusStrip1.Text = "statusStrip1"; // - // tsslAppId + // tsslDatabaseId // - tsslAppId.Name = "tsslAppId"; - tsslAppId.Padding = new Padding(0, 0, 40, 0); - tsslAppId.Size = new Size(82, 17); - tsslAppId.Text = "AppId:"; + tsslDatabaseId.Name = "tsslDatabaseId"; + tsslDatabaseId.Padding = new Padding(0, 0, 40, 0); + tsslDatabaseId.Size = new Size(82, 17); + tsslDatabaseId.Text = "Database ID:"; // // tsslAuthToken // @@ -334,7 +334,7 @@ private void InitializeComponent() #endregion private StatusStrip statusStrip1; - private ToolStripStatusLabel tsslAppId; + private ToolStripStatusLabel tsslDatabaseId; private ToolStripStatusLabel tsslAuthToken; private MenuStrip menuStrip1; private ToolStripMenuItem fileToolStripMenuItem; diff --git a/dotnet-winforms/TasksApp/MainForm.cs b/dotnet-winforms/TasksApp/MainForm.cs index b8e86546d..9cbf603d1 100644 --- a/dotnet-winforms/TasksApp/MainForm.cs +++ b/dotnet-winforms/TasksApp/MainForm.cs @@ -32,8 +32,8 @@ public MainForm(TasksPeer tasksPeer) private void SetFormValues() { - tsslAppId.Text = $"AppId: {_tasksPeer.AppId}"; - tsslAuthToken.Text = $"Online Playground Token: {_tasksPeer.PlaygroundToken}"; + tsslDatabaseId.Text = $"Database ID: {_tasksPeer.DatabaseId}"; + tsslAuthToken.Text = $"Development Token: {_tasksPeer.DevelopmentToken}"; tsbSyncStatus.Image = Image.FromFile(_imageSyncOn); diff --git a/dotnet-winforms/TasksApp/Program.cs b/dotnet-winforms/TasksApp/Program.cs index 6394c2e2e..449e7390b 100644 --- a/dotnet-winforms/TasksApp/Program.cs +++ b/dotnet-winforms/TasksApp/Program.cs @@ -13,12 +13,11 @@ internal static class Program static async Task Main() { var env = LoadEnvVariables(); - var appId = env["DITTO_APP_ID"]; - var playgroundToken = env["DITTO_PLAYGROUND_TOKEN"]; - var websocketUrl = env["DITTO_WEBSOCKET_URL"]; - var authUrl = env["DITTO_AUTH_URL"]; + var databaseId = env["DITTO_DATABASE_ID"]; + var developmentToken = env["DITTO_DEVELOPMENT_TOKEN"]; + var serverUrl = env["DITTO_SERVER_URL"]; - using var peer = await TasksPeer.Create(appId, playgroundToken, authUrl, websocketUrl); + using var peer = await TasksPeer.Create(databaseId, developmentToken, serverUrl); // Disable Ditto's standard-error logging DittoLogger.IsEnabled = true; @@ -81,4 +80,4 @@ private static Dictionary LoadEnvVariables() return envVars; } } -} \ No newline at end of file +} diff --git a/dotnet-winforms/TasksApp/TasksPeer.cs b/dotnet-winforms/TasksApp/TasksPeer.cs index 59dfe2935..d8779e2ae 100644 --- a/dotnet-winforms/TasksApp/TasksPeer.cs +++ b/dotnet-winforms/TasksApp/TasksPeer.cs @@ -18,12 +18,15 @@ namespace DittoTasksApp { public class TasksPeer : IDisposable { - private const string Query = "SELECT * FROM tasks WHERE NOT deleted"; + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; - public string AppId { get; private set; } - public string PlaygroundToken { get; private set; } - public string AuthUrl { get; private set; } - public string WebsocketUrl { get; private set; } + public string DatabaseId { get; private set; } + public string DevelopmentToken { get; private set; } + public string ServerUrl { get; private set; } public bool IsSyncActive => _ditto.Sync.IsActive; @@ -32,19 +35,17 @@ public class TasksPeer : IDisposable /// /// Creates a new synchronizing TasksPeer instance. /// - public static async Task Create( - string appId, - string playgroundToken, - string authUrl, - string websocketUrl) + public static Task Create( + string databaseId, + string developmentToken, + string serverUrl) { - var peer = new TasksPeer(appId, playgroundToken, authUrl, websocketUrl); + var peer = new TasksPeer(databaseId, developmentToken, serverUrl); peer.Authenticate(); peer.RegisterSubscription(); - await peer.InsertInitialTasks(); peer.StartSync(); - return peer; + return Task.FromResult(peer); } private void Authenticate() @@ -56,8 +57,8 @@ private void Authenticate() { await ditto.Auth.LoginAsync( // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider DittoAuthenticationProvider.Development ); Console.WriteLine("Authentication successful"); @@ -75,9 +76,9 @@ await ditto.Auth.LoginAsync( /// will be synced to this peer during the synchronization process. /// /// - /// The subscription is created using the same query that filters out deleted tasks - /// (SELECT * FROM tasks WHERE NOT deleted), ensuring that only active, - /// non-deleted tasks are synchronized across the network. + /// The subscription syncs all tasks (SELECT * FROM tasks) and controls + /// what is written to this peer's local database; the observer query hides + /// soft-deleted tasks from the displayed list. /// /// This method should be called during peer initialization to establish the /// subscription before starting the sync process. The subscription remains active @@ -88,26 +89,24 @@ private void RegisterSubscription() { // Register a subscription, which determines what data syncs to this peer // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - _ditto.Sync.RegisterSubscription(Query); + _ditto.Sync.RegisterSubscription(SubscriptionQuery); } /// /// Constructor /// - /// Ditto application ID - /// Ditto online playground token - /// Ditto Auth URL - /// Ditto Websocket URL - private TasksPeer(string appId, string playgroundToken, string authUrl, string websocketUrl) + /// Ditto database ID + /// Ditto development token + /// Ditto Server URL + private TasksPeer(string databaseId, string developmentToken, string serverUrl) { - AppId = appId; - PlaygroundToken = playgroundToken; - AuthUrl = authUrl; - WebsocketUrl = websocketUrl; + DatabaseId = databaseId; + DevelopmentToken = developmentToken; + ServerUrl = serverUrl; var config = new DittoConfig( - AppId, - new DittoConfigConnect.Server(new Uri(authUrl)) + DatabaseId, + new DittoConfigConnect.Server(new Uri(serverUrl)) ); _ditto = Ditto.Open(config); @@ -119,53 +118,6 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Inserts the initial tasks into the 'tasks' collection. - /// - private async Task InsertInitialTasks() - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - const string insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - /// /// Adds a new task to the 'tasks' collection. /// @@ -182,10 +134,10 @@ public async Task AddTask(string title) {"done", false}, {"deleted", false } }; - const string insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; + const string insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() { - { "doc", doc } + { "task", doc } }); } @@ -234,10 +186,10 @@ public async Task DeleteTask(string taskId) /// public async Task UpdateTaskDone(string taskId, bool newDoneState) { - const string updateQuery = "UPDATE tasks SET done = :newDoneState WHERE _id = :id AND done != :newDoneState"; + const string updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary { - { "newDoneState", newDoneState }, + { "done", newDoneState }, { "id", taskId } }); } @@ -249,7 +201,7 @@ public DittoStoreObserver ObserveTasksCollection(Func, Task> han { // Register an observer, which runs against the local database on this peer // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - return _ditto.Store.RegisterObserver(Query, async (queryResult) => + return _ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => { try { diff --git a/electron/README.md b/electron/README.md index b9ec00a9a..322be0e5b 100644 --- a/electron/README.md +++ b/electron/README.md @@ -5,7 +5,7 @@ This directory contains Ditto's quickstart app for [Electron](https://www.electr ## Documentation - [Javascript Install Guide](https://docs.ditto.com/sdk/latest/install-guides/js) -- [Javascript API Reference](https://software.ditto.live/js/Ditto/5.0.0/api-reference/) +- [Javascript API Reference](https://docs.ditto.live/sdk/latest/api-reference/js) - [Javascript Release Notes](https://docs.ditto.com/sdk/latest/release-notes/js) ## Prerequisites @@ -36,7 +36,7 @@ The renderer never imports `@dittolive/ditto`; it only calls `window.ditto.*` me ### Peer-to-peer transports -This quickstart enables LAN sync (TCP + mDNS + multicast) and disables Bluetooth LE and AWDL. BLE and AWDL on macOS require entitlements that only signed app bundles get; a plain `npm run dev` Electron process can't use them. LAN P2P provides peer-to-peer sync between devices on the same network, and the `DITTO_WEBSOCKET_URL` provides cloud sync to Ditto's Big Peer. +This quickstart enables LAN sync (TCP + mDNS + multicast) and disables Bluetooth LE and AWDL. BLE and AWDL on macOS require entitlements that only signed app bundles get; a plain `npm run dev` Electron process can't use them. LAN P2P provides peer-to-peer sync between devices on the same network, and the `DITTO_SERVER_URL` provides cloud sync to Ditto's Big Peer. ### Local storage @@ -50,7 +50,7 @@ Tasks survive restarts because of this. To reset, quit the app and delete that d ## Getting Started -To get started, you'll first need to create an app in the [Ditto Portal][0] with the "Online Playground" authentication type. You'll need your AppID, Playground Token, Auth URL, and WebSocket URL. +To get started, you'll first need to create an app in the [Ditto Portal][0] with the "Development" authentication type. You'll need your Database ID, Development Token, and Server URL. [0]: https://portal.ditto.live @@ -63,10 +63,9 @@ cp .env.sample .env The `.env` file should look like this (with your fields filled in): ```bash -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" -DITTO_WEBSOCKET_URL="" +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" ``` Then run the app: diff --git a/electron/package-lock.json b/electron/package-lock.json index 9501f5a7b..5e0b0e50b 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -8,7 +8,7 @@ "name": "ditto-quickstart-electron", "version": "0.0.0", "dependencies": { - "@dittolive/ditto": "5.0.0", + "@dittolive/ditto": "5.0.2", "dotenv": "^16.4.5" }, "devDependencies": { @@ -16,10 +16,10 @@ "@types/node": "^20.14.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.20", "electron": "^42.0.0", - "electron-vite": "^5.0.0", + "electron-vite": "^6.0.0-beta.1", "eslint": "^9.15.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.0", @@ -33,7 +33,7 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.4.2" + "vite": "^8.0.10" }, "engines": { "node": ">=22.12.0" @@ -319,9 +319,9 @@ } }, "node_modules/@dittolive/ditto": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.0.tgz", - "integrity": "sha512-Qv4bXhaXbCJlazFa2nLsx6LLeFohBuT0GupfXo7R+nalNGv5kzrJMuafbawydIuaP7ZgGn2e+39eiSny8iRSHg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.2.tgz", + "integrity": "sha512-RsgEtYfTpSv9sIYn6GXDLwS+RHJSQz+Obzpub86k9hvwFULSOMrvVZWklsWwbI7fGJYBI/ej9LlBrlWoZ+zTLw==", "license": "SEE LICENSE IN LICENSE.md", "engines": { "node": ">=14" @@ -361,10 +361,44 @@ "node": ">=10" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -379,9 +413,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -396,9 +430,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -413,9 +447,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -430,9 +464,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -447,9 +481,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -464,9 +498,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -481,9 +515,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -498,9 +532,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -515,9 +549,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -532,9 +566,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -549,9 +583,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -566,9 +600,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -583,9 +617,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -600,9 +634,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -617,9 +651,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -634,9 +668,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -651,9 +685,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -668,9 +702,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -685,9 +719,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -702,9 +736,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -719,9 +753,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -736,9 +770,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -753,9 +787,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -770,9 +804,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -787,9 +821,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1076,6 +1110,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1114,6 +1167,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -1127,31 +1190,10 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", - "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", - "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -1160,12 +1202,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", - "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -1174,12 +1219,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", - "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -1188,26 +1236,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", - "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", - "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -1216,26 +1253,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", - "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", - "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -1244,180 +1270,135 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", - "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", - "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", - "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", - "cpu": [ - "loong64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", - "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", - "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", - "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", - "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", - "cpu": [ - "riscv64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", - "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", - "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", - "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", - "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", - "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", - "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -1426,40 +1407,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", - "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", - "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", - "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -1468,32 +1460,28 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", - "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz", - "integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.46.tgz", + "integrity": "sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.26" + "@swc/types": "^0.1.27" }, "engines": { "node": ">=10" @@ -1503,18 +1491,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.33", - "@swc/core-darwin-x64": "1.15.33", - "@swc/core-linux-arm-gnueabihf": "1.15.33", - "@swc/core-linux-arm64-gnu": "1.15.33", - "@swc/core-linux-arm64-musl": "1.15.33", - "@swc/core-linux-ppc64-gnu": "1.15.33", - "@swc/core-linux-s390x-gnu": "1.15.33", - "@swc/core-linux-x64-gnu": "1.15.33", - "@swc/core-linux-x64-musl": "1.15.33", - "@swc/core-win32-arm64-msvc": "1.15.33", - "@swc/core-win32-ia32-msvc": "1.15.33", - "@swc/core-win32-x64-msvc": "1.15.33" + "@swc/core-darwin-arm64": "1.15.46", + "@swc/core-darwin-x64": "1.15.46", + "@swc/core-linux-arm-gnueabihf": "1.15.46", + "@swc/core-linux-arm64-gnu": "1.15.46", + "@swc/core-linux-arm64-musl": "1.15.46", + "@swc/core-linux-ppc64-gnu": "1.15.46", + "@swc/core-linux-s390x-gnu": "1.15.46", + "@swc/core-linux-x64-gnu": "1.15.46", + "@swc/core-linux-x64-musl": "1.15.46", + "@swc/core-win32-arm64-msvc": "1.15.46", + "@swc/core-win32-ia32-msvc": "1.15.46", + "@swc/core-win32-x64-msvc": "1.15.46" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1526,9 +1514,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz", - "integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.46.tgz", + "integrity": "sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==", "cpu": [ "arm64" ], @@ -1543,9 +1531,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz", - "integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.46.tgz", + "integrity": "sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==", "cpu": [ "x64" ], @@ -1560,9 +1548,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz", - "integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.46.tgz", + "integrity": "sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==", "cpu": [ "arm" ], @@ -1577,13 +1565,16 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz", - "integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.46.tgz", + "integrity": "sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1594,13 +1585,16 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz", - "integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.46.tgz", + "integrity": "sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1611,13 +1605,16 @@ } }, "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz", - "integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.46.tgz", + "integrity": "sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1628,13 +1625,16 @@ } }, "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz", - "integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.46.tgz", + "integrity": "sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1645,13 +1645,16 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz", - "integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.46.tgz", + "integrity": "sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1662,13 +1665,16 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz", - "integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.46.tgz", + "integrity": "sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1679,9 +1685,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz", - "integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.46.tgz", + "integrity": "sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==", "cpu": [ "arm64" ], @@ -1696,9 +1702,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz", - "integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.46.tgz", + "integrity": "sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==", "cpu": [ "ia32" ], @@ -1713,9 +1719,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz", - "integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.46.tgz", + "integrity": "sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==", "cpu": [ "x64" ], @@ -1737,15 +1743,26 @@ "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", - "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2105,17 +2122,20 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", - "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.2.tgz", + "integrity": "sha512-MlSlmSAYbYwPjGa+CjOAqwij09iPYAQDHwx8osVkwoDamCxXlg+avpkC65Ysv+L/uqUWvRTsnJCKPjVhZih/sA==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.27", - "@swc/core": "^1.12.11" + "@rolldown/pluginutils": "^1.0.1", + "@swc/core": "^1.15.46" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "node_modules/acorn": { @@ -2348,13 +2368,13 @@ } }, "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=20.19.0" } }, "node_modules/callsites": { @@ -2557,6 +2577,16 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2610,17 +2640,17 @@ "license": "ISC" }, "node_modules/electron-vite": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", - "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", + "version": "6.0.0-beta.1", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-6.0.0-beta.1.tgz", + "integrity": "sha512-jltST77AwNxIeTTDtYhnIEA8ZM0RW9jmSJcqS8x/dQcgeeLlxlcJoYNNJ1tv7/Swor0AbXMYteBGdezoAOt+Nw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.4", + "@babel/core": "^7.29.0", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "cac": "^6.7.14", + "cac": "^7.0.0", "esbuild": "^0.25.11", - "magic-string": "^0.30.19", + "magic-string": "^0.30.21", "picocolors": "^1.1.1" }, "bin": { @@ -2631,7 +2661,7 @@ }, "peerDependencies": { "@swc/core": "^1.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@swc/core": { @@ -2690,9 +2720,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2703,32 +2733,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -3469,6 +3499,279 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3602,9 +3905,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -3812,9 +4115,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -3832,7 +4135,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4158,58 +4461,40 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", - "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.3", - "@rollup/rollup-android-arm64": "4.60.3", - "@rollup/rollup-darwin-arm64": "4.60.3", - "@rollup/rollup-darwin-x64": "4.60.3", - "@rollup/rollup-freebsd-arm64": "4.60.3", - "@rollup/rollup-freebsd-x64": "4.60.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", - "@rollup/rollup-linux-arm-musleabihf": "4.60.3", - "@rollup/rollup-linux-arm64-gnu": "4.60.3", - "@rollup/rollup-linux-arm64-musl": "4.60.3", - "@rollup/rollup-linux-loong64-gnu": "4.60.3", - "@rollup/rollup-linux-loong64-musl": "4.60.3", - "@rollup/rollup-linux-ppc64-gnu": "4.60.3", - "@rollup/rollup-linux-ppc64-musl": "4.60.3", - "@rollup/rollup-linux-riscv64-gnu": "4.60.3", - "@rollup/rollup-linux-riscv64-musl": "4.60.3", - "@rollup/rollup-linux-s390x-gnu": "4.60.3", - "@rollup/rollup-linux-x64-gnu": "4.60.3", - "@rollup/rollup-linux-x64-musl": "4.60.3", - "@rollup/rollup-openbsd-x64": "4.60.3", - "@rollup/rollup-openharmony-arm64": "4.60.3", - "@rollup/rollup-win32-arm64-msvc": "4.60.3", - "@rollup/rollup-win32-ia32-msvc": "4.60.3", - "@rollup/rollup-win32-x64-gnu": "4.60.3", - "@rollup/rollup-win32-x64-msvc": "4.60.3", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4440,9 +4725,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -4520,6 +4805,14 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4572,9 +4865,9 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "optional": true, @@ -4638,24 +4931,23 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4664,14 +4956,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -4680,13 +4973,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -4712,28 +5008,10 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { diff --git a/electron/package.json b/electron/package.json index b7bdea6ea..612082031 100644 --- a/electron/package.json +++ b/electron/package.json @@ -20,7 +20,7 @@ "format": "prettier --write ." }, "dependencies": { - "@dittolive/ditto": "5.0.0", + "@dittolive/ditto": "5.1.0", "dotenv": "^16.4.5" }, "devDependencies": { @@ -28,10 +28,10 @@ "@types/node": "^20.14.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.20", "electron": "^42.0.0", - "electron-vite": "^5.0.0", + "electron-vite": "^6.0.0-beta.1", "eslint": "^9.15.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.0", @@ -45,6 +45,10 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.4.2" + "vite": "^8.0.10" + }, + "overrides": { + "undici": "7.28.0", + "esbuild": "0.28.1" } } diff --git a/electron/src/main/ditto.ts b/electron/src/main/ditto.ts index 330dc65ff..94d39c48b 100644 --- a/electron/src/main/ditto.ts +++ b/electron/src/main/ditto.ts @@ -18,14 +18,14 @@ export async function initDitto( onTasksUpdated: (tasks: Task[]) => void, ): Promise { const config = new DittoConfig( - env.appId, - { mode: 'server', url: env.authUrl }, + env.databaseId, + { mode: 'server', url: env.serverUrl }, app.getPath('userData'), ); ditto = await Ditto.open(config); - // Authenticate with the playground token, and re-authenticate when it expires. + // Authenticate with the development token, and re-authenticate when it expires. await ditto.auth.setExpirationHandler(async (instance) => { const result = await instance.auth.login( env.token, @@ -38,10 +38,8 @@ export async function initDitto( // BLE and AWDL require macOS entitlements that only signed app bundles get, // so they're disabled here for unsigned `npm run dev` builds. LAN (TCP + - // mDNS) provides peer-to-peer sync across the local network; the websocket - // URL provides cloud sync to Ditto's Big Peer. + // mDNS) provides peer-to-peer sync across the local network. ditto.updateTransportConfig((cfg) => { - cfg.connect.websocketURLs = [env.websocketUrl]; cfg.peerToPeer.bluetoothLE.isEnabled = false; cfg.peerToPeer.awdl.isEnabled = false; cfg.peerToPeer.lan.isEnabled = true; @@ -56,7 +54,7 @@ export async function initDitto( // https://docs.ditto.com/sdk/latest/crud/observing-data-changes#setting-up-store-observers observer = ditto.store.registerObserver( - 'SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC', + 'SELECT * FROM tasks WHERE NOT deleted', (results) => { currentTasks = results.items.map((item) => item.value); onTasksUpdated(currentTasks); @@ -65,7 +63,7 @@ export async function initDitto( } export function getInfo(): DittoIdentity { - return { appId: env.appId, token: env.token }; + return { databaseId: env.databaseId, token: env.token }; } // Returns the latest observed task list. Lets the renderer hydrate its @@ -85,14 +83,14 @@ export async function createTask(title: string): Promise { // https://docs.ditto.com/sdk/latest/crud/update export async function editTask(id: string, title: string): Promise { - await ditto?.store.execute('UPDATE tasks SET title=:title WHERE _id=:id', { + await ditto?.store.execute('UPDATE tasks SET title = :title WHERE _id = :id', { id, title, }); } export async function toggleTask(id: string, done: boolean): Promise { - await ditto?.store.execute('UPDATE tasks SET done=:done WHERE _id=:id', { + await ditto?.store.execute('UPDATE tasks SET done = :done WHERE _id = :id', { id, done, }); @@ -100,7 +98,7 @@ export async function toggleTask(id: string, done: boolean): Promise { // https://docs.ditto.com/sdk/latest/crud/delete#soft-delete-pattern export async function deleteTask(id: string): Promise { - await ditto?.store.execute('UPDATE tasks SET deleted=true WHERE _id=:id', { + await ditto?.store.execute('UPDATE tasks SET deleted = true WHERE _id = :id', { id, }); } diff --git a/electron/src/main/env.ts b/electron/src/main/env.ts index 4b3778ffd..07d1dfb53 100644 --- a/electron/src/main/env.ts +++ b/electron/src/main/env.ts @@ -4,21 +4,20 @@ import { resolve } from 'node:path'; dotenv.config({ path: resolve(process.cwd(), '..', '.env') }); export const env = { - appId: process.env.DITTO_APP_ID ?? '', - token: process.env.DITTO_PLAYGROUND_TOKEN ?? '', - authUrl: process.env.DITTO_AUTH_URL ?? '', - websocketUrl: process.env.DITTO_WEBSOCKET_URL ?? '', + databaseId: process.env.DITTO_DATABASE_ID ?? '', + token: process.env.DITTO_DEVELOPMENT_TOKEN ?? '', + serverUrl: process.env.DITTO_SERVER_URL ?? '', }; export function assertEnv(): void { - const missing = ( - ['appId', 'token', 'authUrl', 'websocketUrl'] as const - ).filter((k) => !env[k]); + const missing = (['databaseId', 'token', 'serverUrl'] as const).filter( + (k) => !env[k], + ); if (missing.length > 0) { throw new Error( `Missing required env vars: ${missing.join(', ')}. ` + - `Copy .env.sample to .env at the repo root and fill in DITTO_APP_ID, ` + - `DITTO_PLAYGROUND_TOKEN, DITTO_AUTH_URL, DITTO_WEBSOCKET_URL.`, + `Copy .env.sample to .env at the repo root and fill in DITTO_DATABASE_ID, ` + + `DITTO_DEVELOPMENT_TOKEN, DITTO_SERVER_URL.`, ); } } diff --git a/electron/src/renderer/App.tsx b/electron/src/renderer/App.tsx index bedaa5c72..4da25f423 100644 --- a/electron/src/renderer/App.tsx +++ b/electron/src/renderer/App.tsx @@ -6,7 +6,7 @@ import type { Task } from '../types'; const App = () => { const [error, setError] = useState(null); - const [appId, setAppId] = useState(''); + const [databaseId, setDatabaseId] = useState(''); const [token, setToken] = useState(''); const [syncActive, setSyncActive] = useState(true); const [isInitialized, setIsInitialized] = useState(false); @@ -20,7 +20,7 @@ const App = () => { try { const info = await window.ditto.getInfo(); if (!isMounted) return; - setAppId(info.appId); + setDatabaseId(info.databaseId); setToken(info.token); // Subscribe before fetching initial state so updates that arrive // during the round-trip aren't missed. @@ -93,7 +93,7 @@ const App = () => {
{error && } void; @@ -9,7 +9,7 @@ type Props = { }; const DittoInfo: React.FC = ({ - appId, + databaseId, token, syncEnabled, onToggleSync, @@ -21,7 +21,7 @@ const DittoInfo: React.FC = ({ Ditto Tasks
-

App ID: {appId}

+

Database ID: {databaseId}

Token: {token}

If you haven't created a Ditto Portal account, you can sign up at [Ditto Portal](https://portal.ditto.live) or checkout [this video](https://www.youtube.com/watch?v=1aLiDkgl0Dc) on how to setup a Ditto Portal account. > -#### Obtain App ID and Playground Token +#### Obtain Database ID and Development Token - Log in to your Ditto Portal account -- Navigate to your application and obtain the App ID and Playground Token (see [Sync Credentials](https://docs.ditto.live/cloud/portal/getting-sdk-connection-details) +- Navigate to your application and obtain the Database ID and Development Token (see [Sync Credentials](https://docs.ditto.live/cloud/portal/getting-sdk-connection-details) for more details) -Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and put it into the flutter_app directory (same directory as pubspec.yaml) and add your app ID, online playground token, authUrl and websocket URL. The app will crash if you don't provide an .env file. +Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and put it into the flutter_app directory (same directory as pubspec.yaml) and add your Database ID, development token, and serverUrl. The app will crash if you don't provide an .env file. ### 4. Run the Application diff --git a/flutter_app/android/build.gradle b/flutter_app/android/build.gradle index 0c47cbc0a..08282f487 100644 --- a/flutter_app/android/build.gradle +++ b/flutter_app/android/build.gradle @@ -20,6 +20,12 @@ subprojects { // breaks the manifest merger. Bump every Android subproject (notably // :ditto_live) to match. subprojects { + // :app sets its own minSdkVersion and is already evaluated (forced by + // evaluationDependsOn above); registering afterEvaluate on an already- + // evaluated project throws in Gradle 8.10. Skip it here β€” only plugin + // library subprojects (e.g. :ditto_live) need the bump. + if (project.name == 'app') return + afterEvaluate { project -> if (project.plugins.hasPlugin('com.android.application') || project.plugins.hasPlugin('com.android.library')) { diff --git a/flutter_app/android/gradlew.bat b/flutter_app/android/gradlew.bat index 9b42019c7..9d21a2183 100755 --- a/flutter_app/android/gradlew.bat +++ b/flutter_app/android/gradlew.bat @@ -1,94 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/flutter_app/integration_test/app_test.dart b/flutter_app/integration_test/app_test.dart index 4adacedf6..f68eb8d90 100644 --- a/flutter_app/integration_test/app_test.dart +++ b/flutter_app/integration_test/app_test.dart @@ -13,8 +13,9 @@ void main() { await dotenv.load(fileName: ".env"); }); - testWidgets('App loads and syncs with Ditto Cloud', - (WidgetTester tester) async { + testWidgets('App loads and syncs with Ditto Cloud', ( + WidgetTester tester, + ) async { // Initialize app await app.main(); // Allow up to 10 seconds for Ditto to initialise and the first sync @@ -36,17 +37,16 @@ void main() { // Verify add task FAB is present expect(find.byIcon(Icons.add_task), findsOneWidget); - // Verify clear button is present - expect(find.byIcon(Icons.clear), findsOneWidget); - // Look for the test document that should be synced from Ditto cloud. - // The playground can accumulate many documents from previous CI runs, + // The cloud app can accumulate many documents from previous CI runs, // so we poll rather than waiting a fixed amount of time. const testTitle = String.fromEnvironment('TASK_TO_FIND'); if (testTitle.isEmpty) { - throw Exception('TASK_TO_FIND environment variable must be set. ' - 'Build with: --dart-define=TASK_TO_FIND='); + throw Exception( + 'TASK_TO_FIND environment variable must be set. ' + 'Build with: --dart-define=TASK_TO_FIND=', + ); } // Poll every 500 ms for up to 45 seconds to give the cloud sync @@ -63,9 +63,12 @@ void main() { } } - expect(taskFound, isTrue, - reason: - 'Should find test document with title: $testTitle synced from Ditto cloud within ${syncTimeout.inSeconds}s'); + expect( + taskFound, + isTrue, + reason: + 'Should find test document with title: $testTitle synced from Ditto cloud within ${syncTimeout.inSeconds}s', + ); }); }); } diff --git a/flutter_app/ios/Podfile.lock b/flutter_app/ios/Podfile.lock index cd68bce69..5f302fa36 100644 --- a/flutter_app/ios/Podfile.lock +++ b/flutter_app/ios/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (5.0.0): - - DittoFlutter (= 5.0.0) + - ditto_live (5.0.2): + - DittoFlutter (= 5.0.2) - Flutter - - DittoFlutter (5.0.0) + - DittoFlutter (5.0.2) - Flutter (1.0.0) - integration_test (0.0.1): - Flutter diff --git a/flutter_app/lib/dialog.dart b/flutter_app/lib/dialog.dart index 09ba560ae..b32da35ac 100644 --- a/flutter_app/lib/dialog.dart +++ b/flutter_app/lib/dialog.dart @@ -3,10 +3,7 @@ import 'package:flutter/material.dart'; import 'task.dart'; Future showAddTaskDialog(BuildContext context, [Task? task]) => - showDialog( - context: context, - builder: (context) => _Dialog(task), - ); + showDialog(context: context, builder: (context) => _Dialog(task)); class _Dialog extends StatefulWidget { final Task? taskToEdit; @@ -34,10 +31,7 @@ class _DialogState extends State<_Dialog> { content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, - children: [ - _textInput(_name, "Name"), - _doneSwitch, - ], + children: [_textInput(_name, "Name"), _doneSwitch], ), actions: [ TextButton( @@ -47,11 +41,7 @@ class _DialogState extends State<_Dialog> { ElevatedButton( child: Text(widget.taskToEdit == null ? "Add Task" : "Edit Task"), onPressed: () { - final task = Task( - title: _name.text, - done: _done, - deleted: false, - ); + final task = Task(title: _name.text, done: _done, deleted: false); Navigator.of(context).pop(task); }, ), @@ -61,9 +51,7 @@ class _DialogState extends State<_Dialog> { Widget _textInput(TextEditingController controller, String label) => ListTile( title: TextField( controller: controller, - decoration: InputDecoration( - labelText: label, - ), + decoration: InputDecoration(labelText: label), ), ); diff --git a/flutter_app/lib/dql_builder.dart b/flutter_app/lib/dql_builder.dart index 1240dc1f2..ae4dbae14 100644 --- a/flutter_app/lib/dql_builder.dart +++ b/flutter_app/lib/dql_builder.dart @@ -98,12 +98,13 @@ class _DqlBuilderState extends State { if (stream == null) return placeholder; return StreamBuilder( - stream: stream, - builder: (context, snapshot) { - final response = snapshot.data; - if (response == null) return widget.loading ?? _defaultLoading; - return widget.builder(context, response); - }); + stream: stream, + builder: (context, snapshot) { + final response = snapshot.data; + if (response == null) return widget.loading ?? _defaultLoading; + return widget.builder(context, response); + }, + ); } } diff --git a/flutter_app/lib/main.dart b/flutter_app/lib/main.dart index e1ec06f22..fad0ec191 100644 --- a/flutter_app/lib/main.dart +++ b/flutter_app/lib/main.dart @@ -25,12 +25,12 @@ class DittoExample extends StatefulWidget { class _DittoExampleState extends State { Ditto? _ditto; - final appID = - dotenv.env['DITTO_APP_ID'] ?? (throw Exception("env not found")); - final token = dotenv.env['DITTO_PLAYGROUND_TOKEN'] ?? + final databaseId = + dotenv.env['DITTO_DATABASE_ID'] ?? (throw Exception("env not found")); + final token = dotenv.env['DITTO_DEVELOPMENT_TOKEN'] ?? (throw Exception("env not found")); - final authUrl = - dotenv.env['DITTO_AUTH_URL'] ?? (throw Exception("env not found")); + final serverUrl = + dotenv.env['DITTO_SERVER_URL'] ?? (throw Exception("env not found")); @override void initState() { @@ -45,15 +45,17 @@ class _DittoExampleState extends State { /// This function: /// 1. Requests required Bluetooth and WiFi permissions on mobile platforms (Android/iOS) /// 2. Initializes the Ditto SDK - /// 3. Sets up online playground identity with the provided app ID and token + /// 3. Sets up development identity with the provided database ID and token /// 4. Enables peer-to-peer communication on non-web platforms /// 5. Configures WebSocket connection to Ditto cloud /// 6. Disables DQL strict mode /// 7. Starts sync and updates the app state with the configured Ditto instance Future _initDitto() async { // Skip permissions in test mode - they block integration tests - const isTestMode = - bool.fromEnvironment('INTEGRATION_TEST_MODE', defaultValue: false); + const isTestMode = bool.fromEnvironment( + 'INTEGRATION_TEST_MODE', + defaultValue: false, + ); // Only request permissions on mobile platforms (Android/iOS) // Desktop platforms (macOS, Windows, Linux) don't require these permissions @@ -63,7 +65,7 @@ class _DittoExampleState extends State { Permission.bluetoothConnect, Permission.bluetoothAdvertise, Permission.nearbyWifiDevices, - Permission.bluetoothScan + Permission.bluetoothScan, ].request(); } @@ -74,23 +76,31 @@ class _DittoExampleState extends State { //new configuration - https://docs.ditto.live/sdk/latest/ditto-config final config = DittoConfig( - databaseID: appID, connect: DittoConfigConnectServer(url: authUrl)); + databaseID: databaseId, + connect: DittoConfigConnectServer(url: serverUrl), + ); final ditto = await Ditto.open(config); await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) async { - final authResult = await ditto.auth - .login(token: token, provider: Authenticator.developmentProvider); + final authResult = await ditto.auth.login( + token: token, + provider: Authenticator.developmentProvider, + ); if (authResult.exception != null) { throw authResult.exception!; } }); + // The subscription controls what syncs to this device and is written to the + // local database; the DqlBuilder widget's observer reacts to changes in that + // local database, hiding soft-deleted tasks from the displayed list. + // // Register the tasks subscription before starting sync so it is // included in the very first sync exchange with the cloud. // Without this, the subscription is registered later when the // DqlBuilder widget builds, causing a 5–10 second delay on the // first sync cycle. ditto.sync.registerSubscription( - "SELECT * FROM tasks WHERE deleted = false", + "SELECT * FROM tasks", ); ditto.sync.start(); @@ -111,11 +121,6 @@ class _DittoExampleState extends State { ); } - Future _clearTasks() async { - // https://docs.ditto.live/sdk/latest/crud/delete#evicting-data - await _ditto!.store.execute("EVICT FROM tasks WHERE true"); - } - @override Widget build(BuildContext context) { if (_ditto == null) return _loading; @@ -123,13 +128,6 @@ class _DittoExampleState extends State { return Scaffold( appBar: AppBar( title: const Text("Ditto Tasks"), - actions: [ - IconButton( - icon: const Icon(Icons.clear), - tooltip: "Clear", - onPressed: _clearTasks, - ), - ], ), floatingActionButton: _fab, body: Column( @@ -152,8 +150,8 @@ class _DittoExampleState extends State { CrossAxisAlignment.center, // Center horizontally children: [ const CircularProgressIndicator(), - const Text("Ensure your AppID and Token are correct"), - _portalInfo + const Text("Ensure your Database ID and Token are correct"), + _portalInfo, ], ), ), @@ -164,10 +162,9 @@ class _DittoExampleState extends State { child: const Icon(Icons.add_task), ); - Widget get _portalInfo => Column(children: [ - Text("AppID: $appID"), - Text("Token: $token"), - ]); + Widget get _portalInfo => Column( + children: [Text("Database ID: $databaseId"), Text("Token: $token")], + ); Widget get _syncTile => SwitchListTile( title: const Text("Sync Active"), @@ -181,17 +178,12 @@ class _DittoExampleState extends State { }, ); - // Ordering is intentionally omitted here because this screen currently uses - // a single DqlBuilder query, and reintroducing title-based ordering would - // require a different query approach. Widget get _tasksList => DqlBuilder( ditto: _ditto!, - query: "SELECT * FROM tasks WHERE deleted = false", + query: "SELECT * FROM tasks WHERE NOT deleted", builder: (context, result) { final tasks = result.items.map((r) => r.value).map(Task.fromJson); - return ListView( - children: tasks.map(_singleTask).toList(), - ); + return ListView(children: tasks.map(_singleTask).toList()); }, ); @@ -206,9 +198,10 @@ class _DittoExampleState extends State { ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Deleted Task ${task.title}")), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar(content: Text("Deleted Task ${task.title}"))); } }, background: _dismissibleBackground(true), diff --git a/flutter_app/macos/Podfile.lock b/flutter_app/macos/Podfile.lock index f1c633448..b62114025 100644 --- a/flutter_app/macos/Podfile.lock +++ b/flutter_app/macos/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (5.0.0): - - DittoFlutter (= 5.0.0) + - ditto_live (5.0.2): + - DittoFlutter (= 5.0.2) - FlutterMacOS - - DittoFlutter (5.0.0) + - DittoFlutter (5.0.2) - FlutterMacOS (1.0.0) - path_provider_foundation (0.0.1): - Flutter diff --git a/flutter_app/pubspec.lock b/flutter_app/pubspec.lock index 4b2ac731f..a42851a51 100644 --- a/flutter_app/pubspec.lock +++ b/flutter_app/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: ditto_live - sha256: "51853b26df7b1de8495bb3bf6a674b1b15b3c60603e80397e0cdb9a66e44457e" + sha256: "6a75e9e8f0f3cf8223a96b730770aba4c09926065d993bad9180571fde0fd388" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.2" equatable: dependency: "direct main" description: @@ -469,5 +469,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.0" diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 7244f6995..86131e0d6 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -1,8 +1,8 @@ name: flutter_quickstart -description: "Demonstrates how to use the ditto_live plugin." +description: 'Demonstrates how to use the ditto_live plugin.' # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: "none" # Remove this line if you wish to publish to pub.dev +publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -19,8 +19,8 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=3.3.0 <4.0.0" - flutter: ">=3.24.0" + sdk: '>=3.3.0 <4.0.0' + flutter: '>=3.24.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -32,7 +32,7 @@ dependencies: flutter: sdk: flutter - ditto_live: 5.0.0 + ditto_live: 5.1.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -67,8 +67,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - # environment file for Ditto Config values - - .env + # environment file for Ditto Config values + - .env # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/flutter_app/test/dql_safety_test.dart b/flutter_app/test/dql_safety_test.dart new file mode 100644 index 000000000..3be9fc9f6 --- /dev/null +++ b/flutter_app/test/dql_safety_test.dart @@ -0,0 +1,71 @@ +// Regression test for the parameterized-DQL fix. +// +// Earlier versions of this file built UPDATE statements by interpolating +// user-typed task titles into DQL strings, e.g.: +// +// "UPDATE tasks SET title = '${newTask.title}' where _id = '${task.id}'" +// +// A task titled `'); EVICT FROM tasks WHERE true; --` could break out of +// the quoted value and run arbitrary DQL. The fix switched to named +// parameter binding (`:title`, `:done`, `:id`) with values passed via the +// `arguments:` map. +// +// This test pins the fix as a static-source check: any future change that +// reintroduces Dart string interpolation into the DQL strings in +// `lib/main.dart` fails here loudly. It does not exercise the DQL engine β€” +// a real round-trip test would need a live `Ditto` instance, which lives in +// `integration_test/`. The check is intentionally coarse and operates on +// the file as text. + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('main.dart DQL safety', () { + late String source; + + setUpAll(() { + source = File('lib/main.dart').readAsStringSync(); + }); + + test('uses named parameter binding for every mutable field', () { + expect(source, contains(':task'), reason: 'INSERT binding missing'); + expect(source, contains(':title'), reason: 'updateTitle binding missing'); + expect(source, contains(':done'), reason: 'setDone binding missing'); + expect(source, contains(':id'), + reason: 'WHERE _id = :id binding missing'); + }); + + test('no Dart string interpolation inside DQL execute() calls', () { + // Find every `_ditto!.store.execute(` (or `.execute(`) call site and + // inspect the string literal that follows up to the next `,` or `)`. + // Forbid `${` and `$identifier` inside those literals. + final calls = RegExp(r'\.execute\(\s*"([^"]*)"').allMatches(source); + expect( + calls.isNotEmpty, + isTrue, + reason: 'No execute() calls found β€” has main.dart moved?', + ); + + final shortInterp = RegExp(r'\$[a-zA-Z_{][a-zA-Z0-9_]*'); + for (final m in calls) { + final query = m.group(1)!; + expect( + query, + isNot(contains(r'${')), + reason: 'Found `\${` inside DQL: $query β€” use named binding.', + ); + final hit = shortInterp.firstMatch(query); + expect( + hit, + isNull, + reason: hit == null + ? '' + : 'Found `${hit.group(0)}` (Dart interpolation) inside DQL: ' + '$query β€” replace with `:name` and pass via `arguments:`.', + ); + } + }); + }); +} diff --git a/flutter_app/test/widget_test.dart b/flutter_app/test/widget_test.dart index 1b4d96200..9639126ce 100644 --- a/flutter_app/test/widget_test.dart +++ b/flutter_app/test/widget_test.dart @@ -39,11 +39,7 @@ void main() { }); test('toJson omits null id', () { - const task = Task( - title: 'New task', - done: false, - deleted: false, - ); + const task = Task(title: 'New task', done: false, deleted: false); final json = task.toJson(); @@ -69,8 +65,9 @@ void main() { }); group('Add task dialog', () { - testWidgets('shows Add Task title for new task', - (WidgetTester tester) async { + testWidgets('shows Add Task title for new task', ( + WidgetTester tester, + ) async { await tester.pumpWidget( MaterialApp( home: Builder( @@ -91,8 +88,9 @@ void main() { expect(find.text('Cancel'), findsOneWidget); }); - testWidgets('shows Edit Task title when editing existing task', - (WidgetTester tester) async { + testWidgets('shows Edit Task title when editing existing task', ( + WidgetTester tester, + ) async { const existing = Task( id: '1', title: 'Existing task', diff --git a/flutter_app/windows/CMakeLists.txt b/flutter_app/windows/CMakeLists.txt index 8943541fe..5971c91f9 100644 --- a/flutter_app/windows/CMakeLists.txt +++ b/flutter_app/windows/CMakeLists.txt @@ -53,6 +53,14 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) add_subdirectory("runner") +# permission_handler_windows (0.2.1, latest) requests C++20 but forces the +# legacy coroutine path via `/await`, which pulls in . +# The MSVC 14.51 (VS2026) STL turns that into hard error STL1011. Define the +# silence macro for the targets below (including the plugins added next) until +# the plugin migrates to the C++20 header. See SDKS-3997 and +# upstream Baseflow/flutter-permission-handler#1534. +add_compile_definitions(_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS) + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/global.json b/global.json new file mode 100644 index 000000000..085a86675 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "//": "The quickstart .NET apps target net10.0; this shadows the monorepo root global.json (which pins the 8.x SDK the core repo builds with) so `dotnet` resolves a .NET 10 SDK for everything under quickstart/.", + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/go-tui/README.md b/go-tui/README.md index 17e26f69f..92f96668d 100644 --- a/go-tui/README.md +++ b/go-tui/README.md @@ -7,8 +7,8 @@ a todo list that syncs between multiple peers. ## Getting Started To get started, you'll first need to create an app in the [Ditto Portal][0] -with the "Online Playground" authentication type. You'll need to find your -AppID and Online Playground Token, Auth URL, and Websocket URL in order to use this quickstart. +with the "Development" authentication type. You'll need to find your +Database ID, Development Token, and Server URL in order to use this quickstart. [0]: https://portal.ditto.live @@ -17,18 +17,18 @@ Create a `.env` file in this directory with your Ditto credentials: ```bash # Create .env file cat > .env << 'EOF' -DITTO_APP_ID=your-app-id -DITTO_PLAYGROUND_TOKEN=your-playground-token -DITTO_AUTH_URL=https://your-app-id.cloud.ditto.live +DITTO_DATABASE_ID=your-app-id +DITTO_DEVELOPMENT_TOKEN=your-development-token +DITTO_SERVER_URL=https://your-app-id.cloud.ditto.live EOF ``` Alternatively, you can set these as environment variables: ```bash -export DITTO_APP_ID="your-app-id" -export DITTO_PLAYGROUND_TOKEN="your-playground-token" -export DITTO_AUTH_URL="https://your-app-id.cloud.ditto.live" +export DITTO_DATABASE_ID="your-app-id" +export DITTO_DEVELOPMENT_TOKEN="your-development-token" +export DITTO_SERVER_URL="https://your-app-id.cloud.ditto.live" ``` ## Building diff --git a/go-tui/main.go b/go-tui/main.go index 673703fae..fa34e07b3 100644 --- a/go-tui/main.go +++ b/go-tui/main.go @@ -83,12 +83,12 @@ func main() { } // Get config from environment - appID := os.Getenv("DITTO_APP_ID") - token := os.Getenv("DITTO_PLAYGROUND_TOKEN") - authURL := os.Getenv("DITTO_AUTH_URL") + databaseId := os.Getenv("DITTO_DATABASE_ID") + token := os.Getenv("DITTO_DEVELOPMENT_TOKEN") + serverURL := os.Getenv("DITTO_SERVER_URL") - if appID == "" || token == "" || authURL == "" { - log.Fatal("Missing required environment variables. Please set DITTO_APP_ID, DITTO_PLAYGROUND_TOKEN, and DITTO_AUTH_URL") + if databaseId == "" || token == "" || serverURL == "" { + log.Fatal("Missing required environment variables. Please set DITTO_DATABASE_ID, DITTO_DEVELOPMENT_TOKEN, and DITTO_SERVER_URL") } // Create temp directory for persistence @@ -100,9 +100,9 @@ func main() { // Initialize Ditto with Server connection API config := ditto.DefaultDittoConfig(). - WithDatabaseID(appID). + WithDatabaseID(databaseId). WithPersistenceDirectory(tempDir). - WithConnect(&ditto.DittoConfigConnectServer{URL: authURL}) + WithConnect(&ditto.DittoConfigConnectServer{URL: serverURL}) d, err := ditto.Open(config) if err != nil { @@ -116,7 +116,7 @@ func main() { func(d *ditto.Ditto, timeUntilExpiration time.Duration) { log.Printf("Expiration handler called with time until expiration: %v", timeUntilExpiration) - // For development mode, login with the playground token + // For development mode, login with the development token provider := ditto.DevelopmentAuthenticationProvider() clientInfoJSON, err := d.Auth().Login(token, provider) if err != nil { @@ -154,7 +154,7 @@ func main() { // Create observer for local changes observer, err := d.Store().RegisterObserver( - "SELECT * FROM tasks WHERE deleted = false ORDER BY _id", + "SELECT * FROM tasks WHERE NOT deleted", nil, func(result *ditto.QueryResult) { defer result.Close() @@ -435,7 +435,7 @@ func (a *App) createTask(title string) { } result, err := a.ditto.Store().Execute( - "INSERT INTO tasks VALUES (:task)", + "INSERT INTO tasks DOCUMENTS (:task)", ditto.QueryArguments{"task": task}, ) if err != nil { diff --git a/java-server/README.md b/java-server/README.md index 89a4d2071..a06febe94 100644 --- a/java-server/README.md +++ b/java-server/README.md @@ -6,8 +6,8 @@ For more information, see - [Java Install Guide](https://docs.ditto.live/sdk/lat ## Getting Started -1. Create an application at . Make note of the app ID and online playground token. -2. Copy the `.env.template` file at the top level of the `quickstart` repo to `.env` and add your app ID and online playground token. +1. Create an application at . Make note of the Database ID and development token. +2. Copy the `.env.template` file at the top level of the `quickstart` repo to `.env` and add your Database ID and development token. 3. Synchronize your project with the Gradle file by clicking Build > Sync Project with Gradle Files. 4. Open a terminal and run the following command to launch the app: - `./gradlew bootRun` @@ -16,5 +16,5 @@ For more information, see - [Java Install Guide](https://docs.ditto.live/sdk/lat ## Additional Resources - [Java Roadmap and Support Policy](https://docs.ditto.live/sdk/latest/install-guides/java/roadmap) -- [API Reference](https://software.ditto.live/java/ditto-java/5.0.0/api-reference/) +- [API Reference](https://docs.ditto.live/sdk/latest/api-reference/java) diff --git a/java-server/build.gradle.kts b/java-server/build.gradle.kts index d3099051c..27b61fd17 100644 --- a/java-server/build.gradle.kts +++ b/java-server/build.gradle.kts @@ -31,29 +31,29 @@ spotbugs { dependencies { // ditto-java artifact includes the Java API for Ditto - implementation("com.ditto:ditto-java:5.0.0") + implementation("com.ditto:ditto-java:5.1.0") // This will include binaries for all the supported platforms and architectures - implementation("com.ditto:ditto-binaries:5.0.0") + implementation("com.ditto:ditto-binaries:5.1.0") // To reduce your module artifact's size, consider including just the necessary platforms and architectures /* // macOS Apple Silicon - implementation("com.ditto:ditto-binaries:5.0.0") { + implementation("com.ditto:ditto-binaries:5.1.0") { capabilities { requireCapability("com.ditto:ditto-binaries-macos-arm64") } } // Windows x86_64 - implementation("com.ditto:ditto-binaries:5.0.0") { + implementation("com.ditto:ditto-binaries:5.1.0") { capabilities { requireCapability("com.ditto:ditto-binaries-windows-x64") } } // Linux x86_64 - implementation("com.ditto:ditto-binaries:5.0.0") { + implementation("com.ditto:ditto-binaries:5.1.0") { capabilities { requireCapability("com.ditto:ditto-binaries-linux-x64") } diff --git a/java-server/java-spring-maven/pom.xml b/java-server/java-spring-maven/pom.xml index a8ed7ec43..67e7f24f3 100644 --- a/java-server/java-spring-maven/pom.xml +++ b/java-server/java-spring-maven/pom.xml @@ -34,14 +34,14 @@ com.ditto ditto-java - 5.0.0-preview.3 + 5.1.0 com.ditto ditto-binaries - 5.0.0-preview.3 + 5.1.0 diff --git a/java-server/settings.gradle.kts b/java-server/settings.gradle.kts index 46a5b6491..2dd67738e 100644 --- a/java-server/settings.gradle.kts +++ b/java-server/settings.gradle.kts @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "quickstart-java" diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java index 9002c792a..d612529dc 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java @@ -42,8 +42,8 @@ public String addTask(@RequestParam("title") @Nonnull String title) { } @PostMapping("/tasks/{taskId}/toggle") - public String toggleTaskDone(@PathVariable @Nonnull String taskId) { - taskService.toggleTaskDone(taskId); + public String toggleTaskDone(@PathVariable @Nonnull String taskId, @RequestParam boolean done) { + taskService.setTaskDone(taskId, done); return ""; } diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index e65182c5e..7b27e0fcf 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -44,9 +44,9 @@ public class DittoService implements DisposableBean { * Setup Ditto Config * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing */ - DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_APP_ID) + DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_DATABASE_ID) // .persistenceDirectory("/tmp/ditto-quickstart") - .serverConnect(DittoSecretsConfiguration.DITTO_AUTH_URL) + .serverConnect(DittoSecretsConfiguration.DITTO_SERVER_URL) .build(); this.ditto = DittoFactory.create(dittoConfig); @@ -54,7 +54,7 @@ public class DittoService implements DisposableBean { this.ditto.getAuth().setExpirationHandler((expiringDitto, _timeUntilExpiration) -> expiringDitto.getAuth() .login( - DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, + DittoSecretsConfiguration.DITTO_DEVELOPMENT_TOKEN, DittoAuthenticationProvider.development() ).thenRun(() -> { }) ); diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 0da0e127d..1b4023c13 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -25,10 +25,10 @@ public DittoTaskService(DittoService dittoService) { public void addTask(@Nonnull String title) { try { dittoService.getDitto().getStore().execute( - "INSERT INTO %s DOCUMENTS (:newTask)".formatted(TASKS_COLLECTION_NAME), + "INSERT INTO %s DOCUMENTS (:task)".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() .put( - "newTask", + "task", DittoCborSerializable.Dictionary.buildDictionary() .put("_id", UUID.randomUUID().toString()) .put("title", title) @@ -43,25 +43,16 @@ public void addTask(@Nonnull String title) { } } - public void toggleTaskDone(@Nonnull String taskId) { + public void setTaskDone(@Nonnull String taskId, boolean done) { try { - DittoQueryResult tasks = dittoService.getDitto().getStore().executeRaw( - "SELECT * FROM %s WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), - DittoCborSerializable.Dictionary.buildDictionary() - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); - - boolean isDone = tasks.getItems().get(0).getValue().get("done").asBoolean(); - dittoService.getDitto().getStore().execute( - "UPDATE %s SET done = :done WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + "UPDATE %s SET done = :done WHERE _id = :id".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("done", !isDone) - .put("taskId", taskId) + .put("done", done) + .put("id", taskId) .build() ).toCompletableFuture().join(); - } catch (Error | DittoException e) { + } catch (Error e) { throw new RuntimeException(e); } } @@ -69,10 +60,9 @@ public void toggleTaskDone(@Nonnull String taskId) { public void deleteTask(@Nonnull String taskId) { try { dittoService.getDitto().getStore().execute( - "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + "UPDATE %s SET deleted = true WHERE _id = :id".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("deleted", true) - .put("taskId", taskId) + .put("id", taskId) .build() ).toCompletableFuture().join(); } catch (Error e) { @@ -83,10 +73,10 @@ public void deleteTask(@Nonnull String taskId) { public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { dittoService.getDitto().getStore().execute( - "UPDATE %s SET title = :title WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + "UPDATE %s SET title = :title WHERE _id = :id".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() .put("title", newTitle) - .put("taskId", taskId) + .put("id", taskId) .build() ).toCompletableFuture().join(); } catch (Error e) { @@ -96,8 +86,12 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { @Nonnull public Flux> observeAll() { - final String subscriptionQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); - final String displayQuery = subscriptionQuery + " ORDER BY title ASC"; + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the returned list. + final String subscriptionQuery = "SELECT * FROM %s".formatted(TASKS_COLLECTION_NAME); + final String displayQuery = + "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); return Flux.create(emitter -> { Ditto ditto = dittoService.getDitto(); diff --git a/java-server/src/main/resources/templates/fragments/taskList.html b/java-server/src/main/resources/templates/fragments/taskList.html index bf650424f..c9b99374b 100644 --- a/java-server/src/main/resources/templates/fragments/taskList.html +++ b/java-server/src/main/resources/templates/fragments/taskList.html @@ -1,7 +1,7 @@